Repository: kubernetes-sigs/kubebuilder Branch: master Commit: c32187afc7ef Files: 1314 Total size: 7.0 MB Directory structure: gitextract_fvvnmo3k/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yaml │ │ ├── config.yml │ │ └── feature_request.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── SECURITY.md │ ├── dependabot.yml │ ├── instructions/ │ │ └── kubebuilder.instructions.md │ └── workflows/ │ ├── apidiff.yml │ ├── codeql.yml │ ├── coverage.yml │ ├── cross-platform-tests.yml │ ├── external-plugin.yml │ ├── legacy-webhook-path.yml │ ├── lint-sample.yml │ ├── lint.yml │ ├── release-version-ci.yml │ ├── release.yml │ ├── scorecard.yml │ ├── spaces.yml │ ├── test-alpha-generate.yml │ ├── test-book.yml │ ├── test-devcontainer.yml │ ├── test-e2e-samples.yml │ ├── test-helm-book.yml │ ├── test-helm-samples.yml │ ├── testdata.yml │ └── verify.yml ├── .gitignore ├── .golangci.yml ├── .yamllint ├── .yamllint-helm ├── AGENTS.md ├── CONTRIBUTING.md ├── DESIGN.md ├── LICENSE ├── Makefile ├── OWNERS ├── OWNERS_ALIASES ├── README.md ├── RELEASE.md ├── SECURITY_CONTACTS ├── VERSIONING.md ├── build/ │ └── .goreleaser.yml ├── code-of-conduct.md ├── designs/ │ ├── README.md │ ├── code-generate-image-plugin.md │ ├── crd_version_conversion.md │ ├── discontinue_usage_of_kube_rbac_proxy.md │ ├── extensible-cli-and-scaffolding-plugins-phase-1-5.md │ ├── extensible-cli-and-scaffolding-plugins-phase-1.md │ ├── extensible-cli-and-scaffolding-plugins-phase-2.md │ ├── helm-chart-autogenerate-plugin.md │ ├── helper_to_upgrade_projects_by_rescaffolding.md │ ├── integrating-kubebuilder-and-osdk.md │ ├── simplified-scaffolding.md │ ├── template.md │ └── update_action.md ├── docs/ │ ├── CONTRIBUTING-ROLES.md │ ├── README.md │ ├── book/ │ │ ├── .firebaserc │ │ ├── book.toml │ │ ├── functions/ │ │ │ └── handle-version.js │ │ ├── install-and-build.sh │ │ ├── litgo.sh │ │ ├── markerdocs.sh │ │ ├── src/ │ │ │ ├── SUMMARY.md │ │ │ ├── TODO.md │ │ │ ├── architecture.md │ │ │ ├── cronjob-tutorial/ │ │ │ │ ├── api-design.md │ │ │ │ ├── basic-project.md │ │ │ │ ├── cert-manager.md │ │ │ │ ├── controller-implementation.md │ │ │ │ ├── controller-overview.md │ │ │ │ ├── cronjob-tutorial.md │ │ │ │ ├── empty-main.md │ │ │ │ ├── gvks.md │ │ │ │ ├── main-revisited.md │ │ │ │ ├── new-api.md │ │ │ │ ├── other-api-files.md │ │ │ │ ├── running-webhook.md │ │ │ │ ├── running.md │ │ │ │ ├── testdata/ │ │ │ │ │ ├── emptyapi.go │ │ │ │ │ ├── emptycontroller.go │ │ │ │ │ ├── emptymain.go │ │ │ │ │ ├── finalizer_example.go │ │ │ │ │ └── project/ │ │ │ │ │ ├── .custom-gcl.yml │ │ │ │ │ ├── .devcontainer/ │ │ │ │ │ │ ├── devcontainer.json │ │ │ │ │ │ └── post-install.sh │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── .github/ │ │ │ │ │ │ └── workflows/ │ │ │ │ │ │ ├── lint.yml │ │ │ │ │ │ ├── test-chart.yml │ │ │ │ │ │ ├── test-e2e.yml │ │ │ │ │ │ └── test.yml │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── .golangci.yml │ │ │ │ │ ├── AGENTS.md │ │ │ │ │ ├── Dockerfile │ │ │ │ │ ├── Makefile │ │ │ │ │ ├── PROJECT │ │ │ │ │ ├── README.md │ │ │ │ │ ├── api/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ ├── cronjob_types.go │ │ │ │ │ │ ├── groupversion_info.go │ │ │ │ │ │ └── zz_generated.deepcopy.go │ │ │ │ │ ├── cmd/ │ │ │ │ │ │ └── main.go │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── certmanager/ │ │ │ │ │ │ │ ├── certificate-metrics.yaml │ │ │ │ │ │ │ ├── certificate-webhook.yaml │ │ │ │ │ │ │ ├── issuer.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ └── kustomizeconfig.yaml │ │ │ │ │ │ ├── crd/ │ │ │ │ │ │ │ ├── bases/ │ │ │ │ │ │ │ │ └── batch.tutorial.kubebuilder.io_cronjobs.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ └── kustomizeconfig.yaml │ │ │ │ │ │ ├── default/ │ │ │ │ │ │ │ ├── cert_metrics_manager_patch.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ ├── manager_metrics_patch.yaml │ │ │ │ │ │ │ ├── manager_webhook_patch.yaml │ │ │ │ │ │ │ └── metrics_service.yaml │ │ │ │ │ │ ├── manager/ │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ └── manager.yaml │ │ │ │ │ │ ├── network-policy/ │ │ │ │ │ │ │ ├── allow-metrics-traffic.yaml │ │ │ │ │ │ │ ├── allow-webhook-traffic.yaml │ │ │ │ │ │ │ └── kustomization.yaml │ │ │ │ │ │ ├── prometheus/ │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ ├── monitor.yaml │ │ │ │ │ │ │ └── monitor_tls_patch.yaml │ │ │ │ │ │ ├── rbac/ │ │ │ │ │ │ │ ├── cronjob_admin_role.yaml │ │ │ │ │ │ │ ├── cronjob_editor_role.yaml │ │ │ │ │ │ │ ├── cronjob_viewer_role.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ ├── leader_election_role.yaml │ │ │ │ │ │ │ ├── leader_election_role_binding.yaml │ │ │ │ │ │ │ ├── metrics_auth_role.yaml │ │ │ │ │ │ │ ├── metrics_auth_role_binding.yaml │ │ │ │ │ │ │ ├── metrics_reader_role.yaml │ │ │ │ │ │ │ ├── role.yaml │ │ │ │ │ │ │ ├── role_binding.yaml │ │ │ │ │ │ │ └── service_account.yaml │ │ │ │ │ │ ├── samples/ │ │ │ │ │ │ │ ├── batch_v1_cronjob.yaml │ │ │ │ │ │ │ └── kustomization.yaml │ │ │ │ │ │ └── webhook/ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ ├── manifests.yaml │ │ │ │ │ │ └── service.yaml │ │ │ │ │ ├── dist/ │ │ │ │ │ │ ├── chart/ │ │ │ │ │ │ │ ├── .helmignore │ │ │ │ │ │ │ ├── Chart.yaml │ │ │ │ │ │ │ ├── templates/ │ │ │ │ │ │ │ │ ├── NOTES.txt │ │ │ │ │ │ │ │ ├── _helpers.tpl │ │ │ │ │ │ │ │ ├── cert-manager/ │ │ │ │ │ │ │ │ │ ├── metrics-certs.yaml │ │ │ │ │ │ │ │ │ ├── selfsigned-issuer.yaml │ │ │ │ │ │ │ │ │ └── serving-cert.yaml │ │ │ │ │ │ │ │ ├── crd/ │ │ │ │ │ │ │ │ │ └── cronjobs.batch.tutorial.kubebuilder.io.yaml │ │ │ │ │ │ │ │ ├── manager/ │ │ │ │ │ │ │ │ │ └── manager.yaml │ │ │ │ │ │ │ │ ├── metrics/ │ │ │ │ │ │ │ │ │ └── controller-manager-metrics-service.yaml │ │ │ │ │ │ │ │ ├── prometheus/ │ │ │ │ │ │ │ │ │ └── controller-manager-metrics-monitor.yaml │ │ │ │ │ │ │ │ ├── rbac/ │ │ │ │ │ │ │ │ │ ├── controller-manager.yaml │ │ │ │ │ │ │ │ │ ├── cronjob-admin-role.yaml │ │ │ │ │ │ │ │ │ ├── cronjob-editor-role.yaml │ │ │ │ │ │ │ │ │ ├── cronjob-viewer-role.yaml │ │ │ │ │ │ │ │ │ ├── leader-election-role.yaml │ │ │ │ │ │ │ │ │ ├── leader-election-rolebinding.yaml │ │ │ │ │ │ │ │ │ ├── manager-role.yaml │ │ │ │ │ │ │ │ │ ├── manager-rolebinding.yaml │ │ │ │ │ │ │ │ │ ├── metrics-auth-role.yaml │ │ │ │ │ │ │ │ │ ├── metrics-auth-rolebinding.yaml │ │ │ │ │ │ │ │ │ └── metrics-reader.yaml │ │ │ │ │ │ │ │ └── webhook/ │ │ │ │ │ │ │ │ ├── mutating-webhook-configuration.yaml │ │ │ │ │ │ │ │ ├── validating-webhook-configuration.yaml │ │ │ │ │ │ │ │ └── webhook-service.yaml │ │ │ │ │ │ │ └── values.yaml │ │ │ │ │ │ └── install.yaml │ │ │ │ │ ├── go.mod │ │ │ │ │ ├── go.sum │ │ │ │ │ ├── hack/ │ │ │ │ │ │ └── boilerplate.go.txt │ │ │ │ │ ├── internal/ │ │ │ │ │ │ ├── controller/ │ │ │ │ │ │ │ ├── cronjob_controller.go │ │ │ │ │ │ │ ├── cronjob_controller_test.go │ │ │ │ │ │ │ └── suite_test.go │ │ │ │ │ │ └── webhook/ │ │ │ │ │ │ └── v1/ │ │ │ │ │ │ ├── cronjob_webhook.go │ │ │ │ │ │ ├── cronjob_webhook_test.go │ │ │ │ │ │ └── webhook_suite_test.go │ │ │ │ │ └── test/ │ │ │ │ │ ├── e2e/ │ │ │ │ │ │ ├── e2e_suite_test.go │ │ │ │ │ │ └── e2e_test.go │ │ │ │ │ └── utils/ │ │ │ │ │ └── utils.go │ │ │ │ ├── webhook-implementation.md │ │ │ │ └── writing-tests.md │ │ │ ├── faq.md │ │ │ ├── getting-started/ │ │ │ │ └── testdata/ │ │ │ │ └── project/ │ │ │ │ ├── .custom-gcl.yml │ │ │ │ ├── .devcontainer/ │ │ │ │ │ ├── devcontainer.json │ │ │ │ │ └── post-install.sh │ │ │ │ ├── .dockerignore │ │ │ │ ├── .github/ │ │ │ │ │ └── workflows/ │ │ │ │ │ ├── auto_update.yml │ │ │ │ │ ├── lint.yml │ │ │ │ │ ├── test-chart.yml │ │ │ │ │ ├── test-e2e.yml │ │ │ │ │ └── test.yml │ │ │ │ ├── .gitignore │ │ │ │ ├── .golangci.yml │ │ │ │ ├── AGENTS.md │ │ │ │ ├── Dockerfile │ │ │ │ ├── Makefile │ │ │ │ ├── PROJECT │ │ │ │ ├── README.md │ │ │ │ ├── api/ │ │ │ │ │ └── v1alpha1/ │ │ │ │ │ ├── groupversion_info.go │ │ │ │ │ ├── memcached_types.go │ │ │ │ │ └── zz_generated.deepcopy.go │ │ │ │ ├── cmd/ │ │ │ │ │ └── main.go │ │ │ │ ├── config/ │ │ │ │ │ ├── crd/ │ │ │ │ │ │ ├── bases/ │ │ │ │ │ │ │ └── cache.example.com_memcacheds.yaml │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ └── kustomizeconfig.yaml │ │ │ │ │ ├── default/ │ │ │ │ │ │ ├── cert_metrics_manager_patch.yaml │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ ├── manager_metrics_patch.yaml │ │ │ │ │ │ └── metrics_service.yaml │ │ │ │ │ ├── manager/ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ └── manager.yaml │ │ │ │ │ ├── network-policy/ │ │ │ │ │ │ ├── allow-metrics-traffic.yaml │ │ │ │ │ │ └── kustomization.yaml │ │ │ │ │ ├── prometheus/ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ ├── monitor.yaml │ │ │ │ │ │ └── monitor_tls_patch.yaml │ │ │ │ │ ├── rbac/ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ ├── leader_election_role.yaml │ │ │ │ │ │ ├── leader_election_role_binding.yaml │ │ │ │ │ │ ├── memcached_admin_role.yaml │ │ │ │ │ │ ├── memcached_editor_role.yaml │ │ │ │ │ │ ├── memcached_viewer_role.yaml │ │ │ │ │ │ ├── metrics_auth_role.yaml │ │ │ │ │ │ ├── metrics_auth_role_binding.yaml │ │ │ │ │ │ ├── metrics_reader_role.yaml │ │ │ │ │ │ ├── role.yaml │ │ │ │ │ │ ├── role_binding.yaml │ │ │ │ │ │ └── service_account.yaml │ │ │ │ │ └── samples/ │ │ │ │ │ ├── cache_v1alpha1_memcached.yaml │ │ │ │ │ └── kustomization.yaml │ │ │ │ ├── dist/ │ │ │ │ │ ├── chart/ │ │ │ │ │ │ ├── .helmignore │ │ │ │ │ │ ├── Chart.yaml │ │ │ │ │ │ ├── templates/ │ │ │ │ │ │ │ ├── NOTES.txt │ │ │ │ │ │ │ ├── _helpers.tpl │ │ │ │ │ │ │ ├── crd/ │ │ │ │ │ │ │ │ └── memcacheds.cache.example.com.yaml │ │ │ │ │ │ │ ├── manager/ │ │ │ │ │ │ │ │ └── manager.yaml │ │ │ │ │ │ │ ├── metrics/ │ │ │ │ │ │ │ │ └── controller-manager-metrics-service.yaml │ │ │ │ │ │ │ ├── monitoring/ │ │ │ │ │ │ │ │ └── servicemonitor.yaml │ │ │ │ │ │ │ └── rbac/ │ │ │ │ │ │ │ ├── controller-manager.yaml │ │ │ │ │ │ │ ├── leader-election-role.yaml │ │ │ │ │ │ │ ├── leader-election-rolebinding.yaml │ │ │ │ │ │ │ ├── manager-role.yaml │ │ │ │ │ │ │ ├── manager-rolebinding.yaml │ │ │ │ │ │ │ ├── memcached-admin-role.yaml │ │ │ │ │ │ │ ├── memcached-editor-role.yaml │ │ │ │ │ │ │ ├── memcached-viewer-role.yaml │ │ │ │ │ │ │ ├── metrics-auth-role.yaml │ │ │ │ │ │ │ ├── metrics-auth-rolebinding.yaml │ │ │ │ │ │ │ └── metrics-reader.yaml │ │ │ │ │ │ └── values.yaml │ │ │ │ │ └── install.yaml │ │ │ │ ├── go.mod │ │ │ │ ├── go.sum │ │ │ │ ├── hack/ │ │ │ │ │ └── boilerplate.go.txt │ │ │ │ ├── internal/ │ │ │ │ │ └── controller/ │ │ │ │ │ ├── memcached_controller.go │ │ │ │ │ ├── memcached_controller_test.go │ │ │ │ │ └── suite_test.go │ │ │ │ └── test/ │ │ │ │ ├── e2e/ │ │ │ │ │ ├── e2e_suite_test.go │ │ │ │ │ └── e2e_test.go │ │ │ │ └── utils/ │ │ │ │ └── utils.go │ │ │ ├── getting-started.md │ │ │ ├── introduction.md │ │ │ ├── logos/ │ │ │ │ └── README.md │ │ │ ├── migration/ │ │ │ │ ├── ai-helpers.md │ │ │ │ ├── discovery-commands.md │ │ │ │ ├── manual-process.md │ │ │ │ ├── multi-group.md │ │ │ │ ├── namespace-scoped.md │ │ │ │ ├── port-code.md │ │ │ │ └── reorganize-layout.md │ │ │ ├── migrations.md │ │ │ ├── multiversion-tutorial/ │ │ │ │ ├── api-changes.md │ │ │ │ ├── conversion-concepts.md │ │ │ │ ├── conversion.md │ │ │ │ ├── deployment.md │ │ │ │ ├── testdata/ │ │ │ │ │ └── project/ │ │ │ │ │ ├── .custom-gcl.yml │ │ │ │ │ ├── .devcontainer/ │ │ │ │ │ │ ├── devcontainer.json │ │ │ │ │ │ └── post-install.sh │ │ │ │ │ ├── .dockerignore │ │ │ │ │ ├── .github/ │ │ │ │ │ │ └── workflows/ │ │ │ │ │ │ ├── lint.yml │ │ │ │ │ │ ├── test-chart.yml │ │ │ │ │ │ ├── test-e2e.yml │ │ │ │ │ │ └── test.yml │ │ │ │ │ ├── .gitignore │ │ │ │ │ ├── .golangci.yml │ │ │ │ │ ├── AGENTS.md │ │ │ │ │ ├── Dockerfile │ │ │ │ │ ├── Makefile │ │ │ │ │ ├── PROJECT │ │ │ │ │ ├── README.md │ │ │ │ │ ├── api/ │ │ │ │ │ │ ├── v1/ │ │ │ │ │ │ │ ├── cronjob_conversion.go │ │ │ │ │ │ │ ├── cronjob_types.go │ │ │ │ │ │ │ ├── groupversion_info.go │ │ │ │ │ │ │ └── zz_generated.deepcopy.go │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ ├── cronjob_conversion.go │ │ │ │ │ │ ├── cronjob_types.go │ │ │ │ │ │ ├── groupversion_info.go │ │ │ │ │ │ └── zz_generated.deepcopy.go │ │ │ │ │ ├── cmd/ │ │ │ │ │ │ └── main.go │ │ │ │ │ ├── config/ │ │ │ │ │ │ ├── certmanager/ │ │ │ │ │ │ │ ├── certificate-metrics.yaml │ │ │ │ │ │ │ ├── certificate-webhook.yaml │ │ │ │ │ │ │ ├── issuer.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ └── kustomizeconfig.yaml │ │ │ │ │ │ ├── crd/ │ │ │ │ │ │ │ ├── bases/ │ │ │ │ │ │ │ │ └── batch.tutorial.kubebuilder.io_cronjobs.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ ├── kustomizeconfig.yaml │ │ │ │ │ │ │ └── patches/ │ │ │ │ │ │ │ └── webhook_in_cronjobs.yaml │ │ │ │ │ │ ├── default/ │ │ │ │ │ │ │ ├── cert_metrics_manager_patch.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ ├── manager_metrics_patch.yaml │ │ │ │ │ │ │ ├── manager_webhook_patch.yaml │ │ │ │ │ │ │ └── metrics_service.yaml │ │ │ │ │ │ ├── manager/ │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ └── manager.yaml │ │ │ │ │ │ ├── network-policy/ │ │ │ │ │ │ │ ├── allow-metrics-traffic.yaml │ │ │ │ │ │ │ ├── allow-webhook-traffic.yaml │ │ │ │ │ │ │ └── kustomization.yaml │ │ │ │ │ │ ├── prometheus/ │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ ├── monitor.yaml │ │ │ │ │ │ │ └── monitor_tls_patch.yaml │ │ │ │ │ │ ├── rbac/ │ │ │ │ │ │ │ ├── cronjob_admin_role.yaml │ │ │ │ │ │ │ ├── cronjob_editor_role.yaml │ │ │ │ │ │ │ ├── cronjob_viewer_role.yaml │ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ │ ├── leader_election_role.yaml │ │ │ │ │ │ │ ├── leader_election_role_binding.yaml │ │ │ │ │ │ │ ├── metrics_auth_role.yaml │ │ │ │ │ │ │ ├── metrics_auth_role_binding.yaml │ │ │ │ │ │ │ ├── metrics_reader_role.yaml │ │ │ │ │ │ │ ├── role.yaml │ │ │ │ │ │ │ ├── role_binding.yaml │ │ │ │ │ │ │ └── service_account.yaml │ │ │ │ │ │ ├── samples/ │ │ │ │ │ │ │ ├── batch_v1_cronjob.yaml │ │ │ │ │ │ │ ├── batch_v2_cronjob.yaml │ │ │ │ │ │ │ └── kustomization.yaml │ │ │ │ │ │ └── webhook/ │ │ │ │ │ │ ├── kustomization.yaml │ │ │ │ │ │ ├── manifests.yaml │ │ │ │ │ │ └── service.yaml │ │ │ │ │ ├── dist/ │ │ │ │ │ │ ├── chart/ │ │ │ │ │ │ │ ├── .helmignore │ │ │ │ │ │ │ ├── Chart.yaml │ │ │ │ │ │ │ ├── templates/ │ │ │ │ │ │ │ │ ├── NOTES.txt │ │ │ │ │ │ │ │ ├── _helpers.tpl │ │ │ │ │ │ │ │ ├── cert-manager/ │ │ │ │ │ │ │ │ │ ├── metrics-certs.yaml │ │ │ │ │ │ │ │ │ ├── selfsigned-issuer.yaml │ │ │ │ │ │ │ │ │ └── serving-cert.yaml │ │ │ │ │ │ │ │ ├── crd/ │ │ │ │ │ │ │ │ │ └── cronjobs.batch.tutorial.kubebuilder.io.yaml │ │ │ │ │ │ │ │ ├── manager/ │ │ │ │ │ │ │ │ │ └── manager.yaml │ │ │ │ │ │ │ │ ├── metrics/ │ │ │ │ │ │ │ │ │ └── controller-manager-metrics-service.yaml │ │ │ │ │ │ │ │ ├── prometheus/ │ │ │ │ │ │ │ │ │ └── controller-manager-metrics-monitor.yaml │ │ │ │ │ │ │ │ ├── rbac/ │ │ │ │ │ │ │ │ │ ├── controller-manager.yaml │ │ │ │ │ │ │ │ │ ├── cronjob-admin-role.yaml │ │ │ │ │ │ │ │ │ ├── cronjob-editor-role.yaml │ │ │ │ │ │ │ │ │ ├── cronjob-viewer-role.yaml │ │ │ │ │ │ │ │ │ ├── leader-election-role.yaml │ │ │ │ │ │ │ │ │ ├── leader-election-rolebinding.yaml │ │ │ │ │ │ │ │ │ ├── manager-role.yaml │ │ │ │ │ │ │ │ │ ├── manager-rolebinding.yaml │ │ │ │ │ │ │ │ │ ├── metrics-auth-role.yaml │ │ │ │ │ │ │ │ │ ├── metrics-auth-rolebinding.yaml │ │ │ │ │ │ │ │ │ └── metrics-reader.yaml │ │ │ │ │ │ │ │ └── webhook/ │ │ │ │ │ │ │ │ ├── mutating-webhook-configuration.yaml │ │ │ │ │ │ │ │ ├── validating-webhook-configuration.yaml │ │ │ │ │ │ │ │ └── webhook-service.yaml │ │ │ │ │ │ │ └── values.yaml │ │ │ │ │ │ └── install.yaml │ │ │ │ │ ├── go.mod │ │ │ │ │ ├── go.sum │ │ │ │ │ ├── hack/ │ │ │ │ │ │ └── boilerplate.go.txt │ │ │ │ │ ├── internal/ │ │ │ │ │ │ ├── controller/ │ │ │ │ │ │ │ ├── cronjob_controller.go │ │ │ │ │ │ │ ├── cronjob_controller_test.go │ │ │ │ │ │ │ └── suite_test.go │ │ │ │ │ │ └── webhook/ │ │ │ │ │ │ ├── v1/ │ │ │ │ │ │ │ ├── cronjob_webhook.go │ │ │ │ │ │ │ ├── cronjob_webhook_test.go │ │ │ │ │ │ │ └── webhook_suite_test.go │ │ │ │ │ │ └── v2/ │ │ │ │ │ │ ├── cronjob_webhook.go │ │ │ │ │ │ ├── cronjob_webhook_test.go │ │ │ │ │ │ └── webhook_suite_test.go │ │ │ │ │ └── test/ │ │ │ │ │ ├── e2e/ │ │ │ │ │ │ ├── e2e_suite_test.go │ │ │ │ │ │ └── e2e_test.go │ │ │ │ │ └── utils/ │ │ │ │ │ └── utils.go │ │ │ │ ├── tutorial.md │ │ │ │ └── webhooks.md │ │ │ ├── plugins/ │ │ │ │ ├── available/ │ │ │ │ │ ├── autoupdate-v1-alpha.md │ │ │ │ │ ├── deploy-image-plugin-v1-alpha.md │ │ │ │ │ ├── go-v4-plugin.md │ │ │ │ │ ├── grafana-v1-alpha.md │ │ │ │ │ ├── helm-v1-alpha.md │ │ │ │ │ ├── helm-v2-alpha.md │ │ │ │ │ └── kustomize-v2.md │ │ │ │ ├── available-plugins.md │ │ │ │ ├── extending/ │ │ │ │ │ ├── custom-markers.md │ │ │ │ │ ├── extending_cli_features_and_plugins.md │ │ │ │ │ ├── external-plugins.md │ │ │ │ │ └── testing-plugins.md │ │ │ │ ├── extending.md │ │ │ │ ├── kustomize-v2.md │ │ │ │ ├── plugins-versioning.md │ │ │ │ ├── plugins.md │ │ │ │ ├── to-add-optional-features.md │ │ │ │ ├── to-be-extended.md │ │ │ │ └── to-scaffold-project.md │ │ │ ├── quick-start.md │ │ │ ├── reference/ │ │ │ │ ├── admission-webhook.md │ │ │ │ ├── alpha_commands.md │ │ │ │ ├── artifacts.md │ │ │ │ ├── commands/ │ │ │ │ │ ├── alpha_generate.md │ │ │ │ │ └── alpha_update.md │ │ │ │ ├── completion.md │ │ │ │ ├── controller-gen.md │ │ │ │ ├── crd-scope.md │ │ │ │ ├── envtest.md │ │ │ │ ├── generating-crd.md │ │ │ │ ├── good-practices.md │ │ │ │ ├── kind-config.yaml │ │ │ │ ├── kind.md │ │ │ │ ├── manager-scope.md │ │ │ │ ├── markers/ │ │ │ │ │ ├── crd-processing.md │ │ │ │ │ ├── crd-validation.md │ │ │ │ │ ├── crd.md │ │ │ │ │ ├── object.md │ │ │ │ │ ├── rbac.md │ │ │ │ │ ├── scaffold.md │ │ │ │ │ └── webhook.md │ │ │ │ ├── markers.md │ │ │ │ ├── metrics-reference.md │ │ │ │ ├── metrics.md │ │ │ │ ├── platform.md │ │ │ │ ├── pprof-tutorial.md │ │ │ │ ├── project-config.md │ │ │ │ ├── raising-events.md │ │ │ │ ├── reference.md │ │ │ │ ├── scopes.md │ │ │ │ ├── submodule-layouts.md │ │ │ │ ├── using-finalizers.md │ │ │ │ ├── using_an_external_resource.md │ │ │ │ ├── watching-resources/ │ │ │ │ │ ├── predicates-with-watch.md │ │ │ │ │ ├── secondary-owned-resources.md │ │ │ │ │ └── secondary-resources-not-owned.md │ │ │ │ ├── watching-resources.md │ │ │ │ ├── webhook-bootstrap-problem.md │ │ │ │ └── webhook-overview.md │ │ │ └── versions_compatibility_supportability.md │ │ ├── theme/ │ │ │ ├── css/ │ │ │ │ ├── custom.css │ │ │ │ ├── markers.css │ │ │ │ └── version-dropdown.css │ │ │ └── index.hbs │ │ └── utils/ │ │ ├── go.mod │ │ ├── go.sum │ │ ├── litgo/ │ │ │ └── literate.go │ │ ├── markerdocs/ │ │ │ ├── doctypes.go │ │ │ ├── html.go │ │ │ └── main.go │ │ └── plugin/ │ │ ├── input.go │ │ ├── plugin.go │ │ └── utils.go │ ├── kubebuilder_annotation.md │ ├── kubebuilder_v0_v1_difference.md │ ├── migration_guide.md │ ├── testing/ │ │ ├── e2e.md │ │ └── integration.md │ └── windows.md ├── go.mod ├── go.sum ├── hack/ │ ├── docs/ │ │ ├── check.sh │ │ ├── generate.sh │ │ ├── generate_samples.go │ │ └── internal/ │ │ ├── cronjob-tutorial/ │ │ │ ├── api_design.go │ │ │ ├── controller_implementation.go │ │ │ ├── e2e_implementation.go │ │ │ ├── generate_cronjob.go │ │ │ ├── main_revisited.go │ │ │ ├── other_api_files.go │ │ │ ├── sample.go │ │ │ ├── webhook_implementation.go │ │ │ ├── writing_tests_controller.go │ │ │ └── writing_tests_env.go │ │ ├── getting-started/ │ │ │ └── generate_getting_started.go │ │ ├── multiversion-tutorial/ │ │ │ ├── controller_tests_code.go │ │ │ ├── cronjob_v1.go │ │ │ ├── cronjob_v2.go │ │ │ ├── generate_multiversion.go │ │ │ ├── hub.go │ │ │ ├── samples.go │ │ │ └── webhook_v2_implementaton.go │ │ └── utils/ │ │ └── utils.go │ └── test/ │ └── check_go_module.go ├── internal/ │ ├── cli/ │ │ ├── alpha/ │ │ │ ├── generate.go │ │ │ ├── generate_test.go │ │ │ ├── internal/ │ │ │ │ ├── common/ │ │ │ │ │ ├── common.go │ │ │ │ │ ├── common_test.go │ │ │ │ │ └── suite_test.go │ │ │ │ ├── generate.go │ │ │ │ ├── generate_test.go │ │ │ │ ├── update/ │ │ │ │ │ ├── helpers/ │ │ │ │ │ │ ├── conflict.go │ │ │ │ │ │ ├── conflict_test.go │ │ │ │ │ │ ├── download.go │ │ │ │ │ │ ├── download_test.go │ │ │ │ │ │ ├── git_commands.go │ │ │ │ │ │ ├── open_gh_issue.go │ │ │ │ │ │ ├── open_gh_issue_test.go │ │ │ │ │ │ └── suite_test.go │ │ │ │ │ ├── integration_test.go │ │ │ │ │ ├── prepare.go │ │ │ │ │ ├── prepare_test.go │ │ │ │ │ ├── suite_test.go │ │ │ │ │ ├── update.go │ │ │ │ │ ├── update_test.go │ │ │ │ │ ├── validate.go │ │ │ │ │ └── validate_test.go │ │ │ │ └── update.go │ │ │ ├── suite_test.go │ │ │ ├── update.go │ │ │ └── update_test.go │ │ ├── cmd/ │ │ │ └── cmd.go │ │ └── version/ │ │ ├── version.go │ │ └── version_test.go │ └── logging/ │ └── handler.go ├── main.go ├── netlify.toml ├── pkg/ │ ├── cli/ │ │ ├── alpha.go │ │ ├── api.go │ │ ├── cli.go │ │ ├── cli_test.go │ │ ├── cmd_helpers.go │ │ ├── cmd_helpers_test.go │ │ ├── completion.go │ │ ├── completion_test.go │ │ ├── create.go │ │ ├── doc.go │ │ ├── edit.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── resource.go │ │ ├── resource_test.go │ │ ├── root.go │ │ ├── suite_test.go │ │ ├── version.go │ │ ├── version_test.go │ │ ├── webhook.go │ │ └── webhook_test.go │ ├── config/ │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── interface.go │ │ ├── registry.go │ │ ├── registry_test.go │ │ ├── store/ │ │ │ ├── errors.go │ │ │ ├── errors_test.go │ │ │ ├── interface.go │ │ │ └── yaml/ │ │ │ ├── store.go │ │ │ └── store_test.go │ │ ├── suite_test.go │ │ ├── v3/ │ │ │ ├── config.go │ │ │ └── config_test.go │ │ ├── version.go │ │ └── version_test.go │ ├── machinery/ │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── file.go │ │ ├── filesystem.go │ │ ├── funcmap.go │ │ ├── funcmap_test.go │ │ ├── injector.go │ │ ├── injector_test.go │ │ ├── interfaces.go │ │ ├── machinery_suite_test.go │ │ ├── marker.go │ │ ├── marker_test.go │ │ ├── mixins.go │ │ ├── mixins_delim_test.go │ │ ├── mixins_test.go │ │ ├── scaffold.go │ │ └── scaffold_test.go │ ├── model/ │ │ ├── resource/ │ │ │ ├── api.go │ │ │ ├── api_test.go │ │ │ ├── gvk.go │ │ │ ├── gvk_test.go │ │ │ ├── resource.go │ │ │ ├── resource_test.go │ │ │ ├── suite_test.go │ │ │ ├── utils.go │ │ │ ├── utils_test.go │ │ │ ├── webhooks.go │ │ │ ├── webhooks_copy_test.go │ │ │ └── webhooks_test.go │ │ └── stage/ │ │ ├── stage.go │ │ └── stage_test.go │ ├── plugin/ │ │ ├── bundle.go │ │ ├── bundle_test.go │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── external/ │ │ │ └── types.go │ │ ├── filter.go │ │ ├── filter_test.go │ │ ├── helpers.go │ │ ├── helpers_test.go │ │ ├── metadata.go │ │ ├── plugin.go │ │ ├── subcommand.go │ │ ├── suite_test.go │ │ ├── util/ │ │ │ ├── exec.go │ │ │ ├── exec_test.go │ │ │ ├── stdin.go │ │ │ ├── stdin_test.go │ │ │ ├── suite_test.go │ │ │ ├── util.go │ │ │ └── util_test.go │ │ ├── version.go │ │ └── version_test.go │ └── plugins/ │ ├── common/ │ │ └── kustomize/ │ │ └── v2/ │ │ ├── api.go │ │ ├── create.go │ │ ├── create_test.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── plugin.go │ │ ├── plugin_test.go │ │ ├── scaffolds/ │ │ │ ├── api.go │ │ │ ├── edit.go │ │ │ ├── init.go │ │ │ ├── internal/ │ │ │ │ └── templates/ │ │ │ │ └── config/ │ │ │ │ ├── certmanager/ │ │ │ │ │ ├── certificate_metrics.go │ │ │ │ │ ├── certificate_webhook.go │ │ │ │ │ ├── issuer.go │ │ │ │ │ ├── kustomization.go │ │ │ │ │ └── kustomizeconfig.go │ │ │ │ ├── crd/ │ │ │ │ │ ├── kustomization.go │ │ │ │ │ ├── kustomizeconfig.go │ │ │ │ │ └── patches/ │ │ │ │ │ ├── enablecainjection_patch.go │ │ │ │ │ └── enablewebhook_patch.go │ │ │ │ ├── kdefault/ │ │ │ │ │ ├── cert_metrics_manager_patch.go │ │ │ │ │ ├── kustomization.go │ │ │ │ │ ├── kustomization_conversion_updater.go │ │ │ │ │ ├── manager_metrics_patch.go │ │ │ │ │ ├── metrics_service.go │ │ │ │ │ └── webhook_manager_patch.go │ │ │ │ ├── manager/ │ │ │ │ │ ├── config.go │ │ │ │ │ └── kustomization.go │ │ │ │ ├── network-policy/ │ │ │ │ │ ├── allow-metrics-traffic.go │ │ │ │ │ ├── allow-webhook-traffic.go │ │ │ │ │ └── kustomization.go │ │ │ │ ├── prometheus/ │ │ │ │ │ ├── kustomization.go │ │ │ │ │ ├── monitor.go │ │ │ │ │ └── monitor_tls_patch.go │ │ │ │ ├── rbac/ │ │ │ │ │ ├── cluster_role.go │ │ │ │ │ ├── cluster_role_binding.go │ │ │ │ │ ├── crd_admin_role.go │ │ │ │ │ ├── crd_editor_role.go │ │ │ │ │ ├── crd_viewer_role.go │ │ │ │ │ ├── kustomization.go │ │ │ │ │ ├── leader_election_role.go │ │ │ │ │ ├── leader_election_role_binding.go │ │ │ │ │ ├── metrics_auth_role.go │ │ │ │ │ ├── metrics_auth_role_binding.go │ │ │ │ │ ├── metrics_reader_role.go │ │ │ │ │ ├── namespaced_role.go │ │ │ │ │ ├── namespaced_role_binding.go │ │ │ │ │ └── service_account.go │ │ │ │ ├── samples/ │ │ │ │ │ ├── crd_sample.go │ │ │ │ │ └── kustomization.go │ │ │ │ └── webhook/ │ │ │ │ ├── kustomization.go │ │ │ │ └── service.go │ │ │ └── webhook.go │ │ ├── suite_test.go │ │ └── webhook.go │ ├── domain.go │ ├── external/ │ │ ├── api.go │ │ ├── edit.go │ │ ├── external_test.go │ │ ├── helpers.go │ │ ├── helpers_test.go │ │ ├── init.go │ │ ├── plugin.go │ │ └── webhook.go │ ├── golang/ │ │ ├── deploy-image/ │ │ │ └── v1alpha1/ │ │ │ ├── api.go │ │ │ ├── api_test.go │ │ │ ├── plugin.go │ │ │ ├── plugin_test.go │ │ │ ├── scaffolds/ │ │ │ │ ├── api.go │ │ │ │ └── internal/ │ │ │ │ └── templates/ │ │ │ │ ├── api/ │ │ │ │ │ └── types.go │ │ │ │ ├── config/ │ │ │ │ │ └── samples/ │ │ │ │ │ └── crd_sample.go │ │ │ │ └── controllers/ │ │ │ │ ├── controller-test.go │ │ │ │ └── controller.go │ │ │ └── suite_test.go │ │ ├── domain.go │ │ ├── go_version.go │ │ ├── go_version_test.go │ │ ├── options.go │ │ ├── options_test.go │ │ ├── repository.go │ │ ├── repository_test.go │ │ ├── suite_test.go │ │ └── v4/ │ │ ├── api.go │ │ ├── api_test.go │ │ ├── edit.go │ │ ├── edit_test.go │ │ ├── init.go │ │ ├── init_test.go │ │ ├── plugin.go │ │ ├── plugin_test.go │ │ ├── scaffolds/ │ │ │ ├── api.go │ │ │ ├── doc.go │ │ │ ├── edit.go │ │ │ ├── edit_integration_test.go │ │ │ ├── init.go │ │ │ ├── init_integration_test.go │ │ │ ├── internal/ │ │ │ │ └── templates/ │ │ │ │ ├── agents.go │ │ │ │ ├── api/ │ │ │ │ │ ├── group.go │ │ │ │ │ ├── hub.go │ │ │ │ │ ├── spoke.go │ │ │ │ │ ├── types.go │ │ │ │ │ └── types_updater.go │ │ │ │ ├── cmd/ │ │ │ │ │ └── main.go │ │ │ │ ├── controllers/ │ │ │ │ │ ├── controller.go │ │ │ │ │ ├── controller_suitetest.go │ │ │ │ │ └── controller_test_template.go │ │ │ │ ├── customgcl.go │ │ │ │ ├── devcontainer.go │ │ │ │ ├── dockerfile.go │ │ │ │ ├── dockerignore.go │ │ │ │ ├── github/ │ │ │ │ │ ├── lint.go │ │ │ │ │ ├── test-e2e.go │ │ │ │ │ └── test.go │ │ │ │ ├── gitignore.go │ │ │ │ ├── golangci.go │ │ │ │ ├── gomod.go │ │ │ │ ├── hack/ │ │ │ │ │ └── boilerplate.go │ │ │ │ ├── makefile.go │ │ │ │ ├── readme.go │ │ │ │ ├── test/ │ │ │ │ │ ├── e2e/ │ │ │ │ │ │ ├── suite.go │ │ │ │ │ │ └── test.go │ │ │ │ │ └── utils/ │ │ │ │ │ └── utils.go │ │ │ │ └── webhooks/ │ │ │ │ ├── webhook.go │ │ │ │ ├── webhook_suitetest.go │ │ │ │ ├── webhook_test_template.go │ │ │ │ ├── webhook_test_updater.go │ │ │ │ └── webhook_updater.go │ │ │ ├── suite_test.go │ │ │ ├── webhook.go │ │ │ └── webhook_test.go │ │ ├── suite_test.go │ │ ├── webhook.go │ │ └── webhook_test.go │ ├── optional/ │ │ ├── autoupdate/ │ │ │ └── v1alpha/ │ │ │ ├── edit.go │ │ │ ├── edit_test.go │ │ │ ├── init.go │ │ │ ├── plugin.go │ │ │ ├── plugin_test.go │ │ │ ├── scaffolds/ │ │ │ │ ├── init.go │ │ │ │ └── internal/ │ │ │ │ └── github/ │ │ │ │ └── auto_update.go │ │ │ └── suite_test.go │ │ ├── grafana/ │ │ │ └── v1alpha/ │ │ │ ├── commons.go │ │ │ ├── constants.go │ │ │ ├── edit.go │ │ │ ├── init.go │ │ │ ├── plugin.go │ │ │ └── scaffolds/ │ │ │ ├── edit.go │ │ │ ├── edit_test.go │ │ │ ├── init.go │ │ │ ├── init_test.go │ │ │ ├── internal/ │ │ │ │ └── templates/ │ │ │ │ ├── custom.go │ │ │ │ ├── custom_metrics.go │ │ │ │ ├── resources.go │ │ │ │ └── runtime.go │ │ │ ├── scaffolds_test.go │ │ │ └── suite_test.go │ │ └── helm/ │ │ ├── v1alpha/ │ │ │ ├── commons.go │ │ │ ├── edit.go │ │ │ ├── plugin.go │ │ │ └── scaffolds/ │ │ │ ├── edit.go │ │ │ └── internal/ │ │ │ └── templates/ │ │ │ ├── chart-templates/ │ │ │ │ ├── cert-manager/ │ │ │ │ │ └── certificate.go │ │ │ │ ├── helpers_tpl.go │ │ │ │ ├── manager/ │ │ │ │ │ └── manager.go │ │ │ │ ├── metrics/ │ │ │ │ │ └── metrics_service.go │ │ │ │ ├── prometheus/ │ │ │ │ │ └── monitor.go │ │ │ │ └── webhook/ │ │ │ │ ├── service.go │ │ │ │ └── webhook.go │ │ │ ├── chart.go │ │ │ ├── github/ │ │ │ │ └── test_chart.go │ │ │ ├── helmignore.go │ │ │ └── values.go │ │ └── v2alpha/ │ │ ├── edit.go │ │ ├── edit_test.go │ │ ├── makefile_test.go │ │ ├── plugin.go │ │ ├── plugin_test.go │ │ ├── scaffolds/ │ │ │ ├── chart_generation_integration_test.go │ │ │ ├── chart_never_overwrite_test.go │ │ │ ├── edit_kustomize.go │ │ │ ├── extras_integration_test.go │ │ │ ├── force_integration_test.go │ │ │ ├── internal/ │ │ │ │ ├── kustomize/ │ │ │ │ │ ├── chart_converter.go │ │ │ │ │ ├── chart_converter_test.go │ │ │ │ │ ├── chart_writer.go │ │ │ │ │ ├── helm_templater.go │ │ │ │ │ ├── helm_templater_test.go │ │ │ │ │ ├── parser.go │ │ │ │ │ ├── parser_test.go │ │ │ │ │ ├── resource_organizer.go │ │ │ │ │ └── suite_test.go │ │ │ │ └── templates/ │ │ │ │ ├── chart-templates/ │ │ │ │ │ ├── consts.go │ │ │ │ │ ├── helpers_tpl.go │ │ │ │ │ ├── notes.go │ │ │ │ │ ├── notes_test.go │ │ │ │ │ ├── servicemonitor.go │ │ │ │ │ └── suite_test.go │ │ │ │ ├── chart.go │ │ │ │ ├── github/ │ │ │ │ │ └── test_chart.go │ │ │ │ ├── helmignore.go │ │ │ │ ├── suite_test.go │ │ │ │ ├── values_basic.go │ │ │ │ └── values_basic_test.go │ │ │ └── suite_test.go │ │ └── suite_test.go │ └── scaffolder.go ├── roadmap/ │ ├── README.md │ ├── roadmap_2024.md │ ├── roadmap_2025.md │ └── roadmap_2026.md ├── test/ │ ├── check-docs-only.sh │ ├── check-license.sh │ ├── check_spaces.sh │ ├── common.sh │ ├── e2e/ │ │ ├── all/ │ │ │ ├── e2e_suite_test.go │ │ │ ├── plugin_deployimage_test.go │ │ │ ├── plugin_helm_test.go │ │ │ └── plugin_v4_test.go │ │ ├── ci.sh │ │ ├── internal/ │ │ │ └── helpers/ │ │ │ ├── generate_v4.go │ │ │ ├── plugin_test_helper.go │ │ │ └── plugin_test_metrics.go │ │ ├── kind-config.yaml │ │ ├── local.sh │ │ ├── setup.sh │ │ └── utils/ │ │ ├── kubectl.go │ │ ├── kubectl_test.go │ │ ├── suite_test.go │ │ ├── test_context.go │ │ └── webhooks.go │ └── testdata/ │ ├── check.sh │ ├── generate.sh │ ├── legacy-webhook-path.sh │ ├── test.sh │ └── test_legacy.sh ├── test.sh ├── test_e2e.sh └── testdata/ ├── project-v4/ │ ├── .custom-gcl.yml │ ├── .devcontainer/ │ │ ├── devcontainer.json │ │ └── post-install.sh │ ├── .dockerignore │ ├── .github/ │ │ └── workflows/ │ │ ├── lint.yml │ │ ├── test-e2e.yml │ │ └── test.yml │ ├── .gitignore │ ├── .golangci.yml │ ├── AGENTS.md │ ├── Dockerfile │ ├── Makefile │ ├── PROJECT │ ├── README.md │ ├── api/ │ │ ├── v1/ │ │ │ ├── admiral_types.go │ │ │ ├── captain_types.go │ │ │ ├── firstmate_conversion.go │ │ │ ├── firstmate_types.go │ │ │ ├── groupversion_info.go │ │ │ ├── sailor_types.go │ │ │ └── zz_generated.deepcopy.go │ │ └── v2/ │ │ ├── firstmate_conversion.go │ │ ├── firstmate_types.go │ │ ├── groupversion_info.go │ │ └── zz_generated.deepcopy.go │ ├── cmd/ │ │ └── main.go │ ├── config/ │ │ ├── certmanager/ │ │ │ ├── certificate-metrics.yaml │ │ │ ├── certificate-webhook.yaml │ │ │ ├── issuer.yaml │ │ │ ├── kustomization.yaml │ │ │ └── kustomizeconfig.yaml │ │ ├── crd/ │ │ │ ├── bases/ │ │ │ │ ├── crew.testproject.org_admirales.yaml │ │ │ │ ├── crew.testproject.org_captains.yaml │ │ │ │ ├── crew.testproject.org_firstmates.yaml │ │ │ │ └── crew.testproject.org_sailors.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── kustomizeconfig.yaml │ │ │ └── patches/ │ │ │ └── webhook_in_firstmates.yaml │ │ ├── default/ │ │ │ ├── cert_metrics_manager_patch.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── manager_metrics_patch.yaml │ │ │ ├── manager_webhook_patch.yaml │ │ │ └── metrics_service.yaml │ │ ├── manager/ │ │ │ ├── kustomization.yaml │ │ │ └── manager.yaml │ │ ├── network-policy/ │ │ │ ├── allow-metrics-traffic.yaml │ │ │ ├── allow-webhook-traffic.yaml │ │ │ └── kustomization.yaml │ │ ├── prometheus/ │ │ │ ├── kustomization.yaml │ │ │ ├── monitor.yaml │ │ │ └── monitor_tls_patch.yaml │ │ ├── rbac/ │ │ │ ├── admiral_admin_role.yaml │ │ │ ├── admiral_editor_role.yaml │ │ │ ├── admiral_viewer_role.yaml │ │ │ ├── captain_admin_role.yaml │ │ │ ├── captain_editor_role.yaml │ │ │ ├── captain_viewer_role.yaml │ │ │ ├── firstmate_admin_role.yaml │ │ │ ├── firstmate_editor_role.yaml │ │ │ ├── firstmate_viewer_role.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── leader_election_role.yaml │ │ │ ├── leader_election_role_binding.yaml │ │ │ ├── metrics_auth_role.yaml │ │ │ ├── metrics_auth_role_binding.yaml │ │ │ ├── metrics_reader_role.yaml │ │ │ ├── role.yaml │ │ │ ├── role_binding.yaml │ │ │ ├── sailor_admin_role.yaml │ │ │ ├── sailor_editor_role.yaml │ │ │ ├── sailor_viewer_role.yaml │ │ │ └── service_account.yaml │ │ ├── samples/ │ │ │ ├── crew_v1_admiral.yaml │ │ │ ├── crew_v1_captain.yaml │ │ │ ├── crew_v1_firstmate.yaml │ │ │ ├── crew_v1_sailor.yaml │ │ │ ├── crew_v2_firstmate.yaml │ │ │ └── kustomization.yaml │ │ └── webhook/ │ │ ├── kustomization.yaml │ │ ├── manifests.yaml │ │ └── service.yaml │ ├── dist/ │ │ └── install.yaml │ ├── go.mod │ ├── hack/ │ │ └── boilerplate.go.txt │ ├── internal/ │ │ ├── controller/ │ │ │ ├── admiral_controller.go │ │ │ ├── admiral_controller_test.go │ │ │ ├── captain_controller.go │ │ │ ├── captain_controller_test.go │ │ │ ├── certificate_controller.go │ │ │ ├── certificate_controller_test.go │ │ │ ├── firstmate_controller.go │ │ │ ├── firstmate_controller_test.go │ │ │ ├── sailor_controller.go │ │ │ ├── sailor_controller_test.go │ │ │ └── suite_test.go │ │ └── webhook/ │ │ └── v1/ │ │ ├── admiral_webhook.go │ │ ├── admiral_webhook_test.go │ │ ├── captain_webhook.go │ │ ├── captain_webhook_test.go │ │ ├── deployment_webhook.go │ │ ├── deployment_webhook_test.go │ │ ├── firstmate_webhook.go │ │ ├── firstmate_webhook_test.go │ │ ├── issuer_webhook.go │ │ ├── issuer_webhook_test.go │ │ ├── pod_webhook.go │ │ ├── pod_webhook_test.go │ │ ├── sailor_webhook.go │ │ ├── sailor_webhook_test.go │ │ └── webhook_suite_test.go │ └── test/ │ ├── e2e/ │ │ ├── e2e_suite_test.go │ │ └── e2e_test.go │ └── utils/ │ └── utils.go ├── project-v4-multigroup/ │ ├── .custom-gcl.yml │ ├── .devcontainer/ │ │ ├── devcontainer.json │ │ └── post-install.sh │ ├── .dockerignore │ ├── .github/ │ │ └── workflows/ │ │ ├── lint.yml │ │ ├── test-e2e.yml │ │ └── test.yml │ ├── .gitignore │ ├── .golangci.yml │ ├── AGENTS.md │ ├── Dockerfile │ ├── Makefile │ ├── PROJECT │ ├── README.md │ ├── api/ │ │ ├── crew/ │ │ │ └── v1/ │ │ │ ├── captain_types.go │ │ │ ├── groupversion_info.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── example.com/ │ │ │ ├── v1/ │ │ │ │ ├── groupversion_info.go │ │ │ │ ├── wordpress_conversion.go │ │ │ │ ├── wordpress_types.go │ │ │ │ └── zz_generated.deepcopy.go │ │ │ ├── v1alpha1/ │ │ │ │ ├── busybox_types.go │ │ │ │ ├── groupversion_info.go │ │ │ │ ├── memcached_types.go │ │ │ │ └── zz_generated.deepcopy.go │ │ │ └── v2/ │ │ │ ├── groupversion_info.go │ │ │ ├── wordpress_conversion.go │ │ │ ├── wordpress_types.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── fiz/ │ │ │ └── v1/ │ │ │ ├── bar_types.go │ │ │ ├── groupversion_info.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── foo/ │ │ │ └── v1/ │ │ │ ├── bar_types.go │ │ │ ├── groupversion_info.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── foo.policy/ │ │ │ └── v1/ │ │ │ ├── groupversion_info.go │ │ │ ├── healthcheckpolicy_types.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── sea-creatures/ │ │ │ ├── v1beta1/ │ │ │ │ ├── groupversion_info.go │ │ │ │ ├── kraken_types.go │ │ │ │ └── zz_generated.deepcopy.go │ │ │ └── v1beta2/ │ │ │ ├── groupversion_info.go │ │ │ ├── leviathan_types.go │ │ │ └── zz_generated.deepcopy.go │ │ └── ship/ │ │ ├── v1/ │ │ │ ├── destroyer_types.go │ │ │ ├── groupversion_info.go │ │ │ └── zz_generated.deepcopy.go │ │ ├── v1beta1/ │ │ │ ├── frigate_types.go │ │ │ ├── groupversion_info.go │ │ │ └── zz_generated.deepcopy.go │ │ └── v2alpha1/ │ │ ├── cruiser_types.go │ │ ├── groupversion_info.go │ │ └── zz_generated.deepcopy.go │ ├── cmd/ │ │ └── main.go │ ├── config/ │ │ ├── certmanager/ │ │ │ ├── certificate-metrics.yaml │ │ │ ├── certificate-webhook.yaml │ │ │ ├── issuer.yaml │ │ │ ├── kustomization.yaml │ │ │ └── kustomizeconfig.yaml │ │ ├── crd/ │ │ │ ├── bases/ │ │ │ │ ├── crew.testproject.org_captains.yaml │ │ │ │ ├── example.com.testproject.org_busyboxes.yaml │ │ │ │ ├── example.com.testproject.org_memcacheds.yaml │ │ │ │ ├── example.com.testproject.org_wordpresses.yaml │ │ │ │ ├── fiz.testproject.org_bars.yaml │ │ │ │ ├── foo.policy.testproject.org_healthcheckpolicies.yaml │ │ │ │ ├── foo.testproject.org_bars.yaml │ │ │ │ ├── sea-creatures.testproject.org_krakens.yaml │ │ │ │ ├── sea-creatures.testproject.org_leviathans.yaml │ │ │ │ ├── ship.testproject.org_cruisers.yaml │ │ │ │ ├── ship.testproject.org_destroyers.yaml │ │ │ │ └── ship.testproject.org_frigates.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── kustomizeconfig.yaml │ │ │ └── patches/ │ │ │ └── webhook_in_example.com_wordpresses.yaml │ │ ├── default/ │ │ │ ├── cert_metrics_manager_patch.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── manager_metrics_patch.yaml │ │ │ ├── manager_webhook_patch.yaml │ │ │ └── metrics_service.yaml │ │ ├── manager/ │ │ │ ├── kustomization.yaml │ │ │ └── manager.yaml │ │ ├── network-policy/ │ │ │ ├── allow-metrics-traffic.yaml │ │ │ ├── allow-webhook-traffic.yaml │ │ │ └── kustomization.yaml │ │ ├── prometheus/ │ │ │ ├── kustomization.yaml │ │ │ ├── monitor.yaml │ │ │ └── monitor_tls_patch.yaml │ │ ├── rbac/ │ │ │ ├── crew_captain_admin_role.yaml │ │ │ ├── crew_captain_editor_role.yaml │ │ │ ├── crew_captain_viewer_role.yaml │ │ │ ├── example.com_busybox_admin_role.yaml │ │ │ ├── example.com_busybox_editor_role.yaml │ │ │ ├── example.com_busybox_viewer_role.yaml │ │ │ ├── example.com_memcached_admin_role.yaml │ │ │ ├── example.com_memcached_editor_role.yaml │ │ │ ├── example.com_memcached_viewer_role.yaml │ │ │ ├── example.com_wordpress_admin_role.yaml │ │ │ ├── example.com_wordpress_editor_role.yaml │ │ │ ├── example.com_wordpress_viewer_role.yaml │ │ │ ├── fiz_bar_admin_role.yaml │ │ │ ├── fiz_bar_editor_role.yaml │ │ │ ├── fiz_bar_viewer_role.yaml │ │ │ ├── foo.policy_healthcheckpolicy_admin_role.yaml │ │ │ ├── foo.policy_healthcheckpolicy_editor_role.yaml │ │ │ ├── foo.policy_healthcheckpolicy_viewer_role.yaml │ │ │ ├── foo_bar_admin_role.yaml │ │ │ ├── foo_bar_editor_role.yaml │ │ │ ├── foo_bar_viewer_role.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── leader_election_role.yaml │ │ │ ├── leader_election_role_binding.yaml │ │ │ ├── metrics_auth_role.yaml │ │ │ ├── metrics_auth_role_binding.yaml │ │ │ ├── metrics_reader_role.yaml │ │ │ ├── role.yaml │ │ │ ├── role_binding.yaml │ │ │ ├── sea-creatures_kraken_admin_role.yaml │ │ │ ├── sea-creatures_kraken_editor_role.yaml │ │ │ ├── sea-creatures_kraken_viewer_role.yaml │ │ │ ├── sea-creatures_leviathan_admin_role.yaml │ │ │ ├── sea-creatures_leviathan_editor_role.yaml │ │ │ ├── sea-creatures_leviathan_viewer_role.yaml │ │ │ ├── service_account.yaml │ │ │ ├── ship_cruiser_admin_role.yaml │ │ │ ├── ship_cruiser_editor_role.yaml │ │ │ ├── ship_cruiser_viewer_role.yaml │ │ │ ├── ship_destroyer_admin_role.yaml │ │ │ ├── ship_destroyer_editor_role.yaml │ │ │ ├── ship_destroyer_viewer_role.yaml │ │ │ ├── ship_frigate_admin_role.yaml │ │ │ ├── ship_frigate_editor_role.yaml │ │ │ └── ship_frigate_viewer_role.yaml │ │ ├── samples/ │ │ │ ├── crew_v1_captain.yaml │ │ │ ├── example.com_v1_wordpress.yaml │ │ │ ├── example.com_v1alpha1_busybox.yaml │ │ │ ├── example.com_v1alpha1_memcached.yaml │ │ │ ├── example.com_v2_wordpress.yaml │ │ │ ├── fiz_v1_bar.yaml │ │ │ ├── foo.policy_v1_healthcheckpolicy.yaml │ │ │ ├── foo_v1_bar.yaml │ │ │ ├── kustomization.yaml │ │ │ ├── sea-creatures_v1beta1_kraken.yaml │ │ │ ├── sea-creatures_v1beta2_leviathan.yaml │ │ │ ├── ship_v1_destroyer.yaml │ │ │ ├── ship_v1beta1_frigate.yaml │ │ │ └── ship_v2alpha1_cruiser.yaml │ │ └── webhook/ │ │ ├── kustomization.yaml │ │ ├── manifests.yaml │ │ └── service.yaml │ ├── dist/ │ │ └── install.yaml │ ├── go.mod │ ├── grafana/ │ │ ├── controller-resources-metrics.json │ │ ├── controller-runtime-metrics.json │ │ └── custom-metrics/ │ │ └── config.yaml │ ├── hack/ │ │ └── boilerplate.go.txt │ ├── internal/ │ │ ├── controller/ │ │ │ ├── apps/ │ │ │ │ ├── deployment_controller.go │ │ │ │ ├── deployment_controller_test.go │ │ │ │ └── suite_test.go │ │ │ ├── cert-manager/ │ │ │ │ ├── certificate_controller.go │ │ │ │ ├── certificate_controller_test.go │ │ │ │ └── suite_test.go │ │ │ ├── crew/ │ │ │ │ ├── captain_controller.go │ │ │ │ ├── captain_controller_test.go │ │ │ │ └── suite_test.go │ │ │ ├── example.com/ │ │ │ │ ├── busybox_controller.go │ │ │ │ ├── busybox_controller_test.go │ │ │ │ ├── memcached_controller.go │ │ │ │ ├── memcached_controller_test.go │ │ │ │ ├── suite_test.go │ │ │ │ ├── wordpress_controller.go │ │ │ │ └── wordpress_controller_test.go │ │ │ ├── fiz/ │ │ │ │ ├── bar_controller.go │ │ │ │ ├── bar_controller_test.go │ │ │ │ └── suite_test.go │ │ │ ├── foo/ │ │ │ │ ├── bar_controller.go │ │ │ │ ├── bar_controller_test.go │ │ │ │ └── suite_test.go │ │ │ ├── foo.policy/ │ │ │ │ ├── healthcheckpolicy_controller.go │ │ │ │ ├── healthcheckpolicy_controller_test.go │ │ │ │ └── suite_test.go │ │ │ ├── sea-creatures/ │ │ │ │ ├── kraken_controller.go │ │ │ │ ├── kraken_controller_test.go │ │ │ │ ├── leviathan_controller.go │ │ │ │ ├── leviathan_controller_test.go │ │ │ │ └── suite_test.go │ │ │ └── ship/ │ │ │ ├── cruiser_controller.go │ │ │ ├── cruiser_controller_test.go │ │ │ ├── destroyer_controller.go │ │ │ ├── destroyer_controller_test.go │ │ │ ├── frigate_controller.go │ │ │ ├── frigate_controller_test.go │ │ │ └── suite_test.go │ │ └── webhook/ │ │ ├── apps/ │ │ │ └── v1/ │ │ │ ├── deployment_webhook.go │ │ │ ├── deployment_webhook_test.go │ │ │ └── webhook_suite_test.go │ │ ├── cert-manager/ │ │ │ └── v1/ │ │ │ ├── issuer_webhook.go │ │ │ ├── issuer_webhook_test.go │ │ │ └── webhook_suite_test.go │ │ ├── core/ │ │ │ └── v1/ │ │ │ ├── pod_webhook.go │ │ │ ├── pod_webhook_test.go │ │ │ └── webhook_suite_test.go │ │ ├── crew/ │ │ │ └── v1/ │ │ │ ├── captain_webhook.go │ │ │ ├── captain_webhook_test.go │ │ │ └── webhook_suite_test.go │ │ ├── example.com/ │ │ │ ├── v1/ │ │ │ │ ├── webhook_suite_test.go │ │ │ │ ├── wordpress_webhook.go │ │ │ │ └── wordpress_webhook_test.go │ │ │ └── v1alpha1/ │ │ │ ├── memcached_webhook.go │ │ │ ├── memcached_webhook_test.go │ │ │ └── webhook_suite_test.go │ │ └── ship/ │ │ ├── v1/ │ │ │ ├── destroyer_webhook.go │ │ │ ├── destroyer_webhook_test.go │ │ │ └── webhook_suite_test.go │ │ └── v2alpha1/ │ │ ├── cruiser_webhook.go │ │ ├── cruiser_webhook_test.go │ │ └── webhook_suite_test.go │ └── test/ │ ├── e2e/ │ │ ├── e2e_suite_test.go │ │ └── e2e_test.go │ └── utils/ │ └── utils.go └── project-v4-with-plugins/ ├── .custom-gcl.yml ├── .devcontainer/ │ ├── devcontainer.json │ └── post-install.sh ├── .dockerignore ├── .github/ │ └── workflows/ │ ├── auto_update.yml │ ├── lint.yml │ ├── test-chart.yml │ ├── test-e2e.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── AGENTS.md ├── Dockerfile ├── Makefile ├── PROJECT ├── README.md ├── api/ │ ├── v1/ │ │ ├── groupversion_info.go │ │ ├── wordpress_conversion.go │ │ ├── wordpress_types.go │ │ └── zz_generated.deepcopy.go │ ├── v1alpha1/ │ │ ├── busybox_types.go │ │ ├── groupversion_info.go │ │ ├── memcached_types.go │ │ └── zz_generated.deepcopy.go │ └── v2/ │ ├── groupversion_info.go │ ├── wordpress_conversion.go │ ├── wordpress_types.go │ └── zz_generated.deepcopy.go ├── cmd/ │ └── main.go ├── config/ │ ├── certmanager/ │ │ ├── certificate-metrics.yaml │ │ ├── certificate-webhook.yaml │ │ ├── issuer.yaml │ │ ├── kustomization.yaml │ │ └── kustomizeconfig.yaml │ ├── crd/ │ │ ├── bases/ │ │ │ ├── example.com.testproject.org_busyboxes.yaml │ │ │ ├── example.com.testproject.org_memcacheds.yaml │ │ │ └── example.com.testproject.org_wordpresses.yaml │ │ ├── kustomization.yaml │ │ ├── kustomizeconfig.yaml │ │ └── patches/ │ │ └── webhook_in_wordpresses.yaml │ ├── default/ │ │ ├── cert_metrics_manager_patch.yaml │ │ ├── kustomization.yaml │ │ ├── manager_metrics_patch.yaml │ │ ├── manager_webhook_patch.yaml │ │ └── metrics_service.yaml │ ├── manager/ │ │ ├── kustomization.yaml │ │ └── manager.yaml │ ├── network-policy/ │ │ ├── allow-metrics-traffic.yaml │ │ ├── allow-webhook-traffic.yaml │ │ └── kustomization.yaml │ ├── prometheus/ │ │ ├── kustomization.yaml │ │ ├── monitor.yaml │ │ └── monitor_tls_patch.yaml │ ├── rbac/ │ │ ├── busybox_admin_role.yaml │ │ ├── busybox_editor_role.yaml │ │ ├── busybox_viewer_role.yaml │ │ ├── kustomization.yaml │ │ ├── leader_election_role.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── memcached_admin_role.yaml │ │ ├── memcached_editor_role.yaml │ │ ├── memcached_viewer_role.yaml │ │ ├── metrics_auth_role.yaml │ │ ├── metrics_auth_role_binding.yaml │ │ ├── metrics_reader_role.yaml │ │ ├── role.yaml │ │ ├── role_binding.yaml │ │ ├── service_account.yaml │ │ ├── wordpress_admin_role.yaml │ │ ├── wordpress_editor_role.yaml │ │ └── wordpress_viewer_role.yaml │ ├── samples/ │ │ ├── example.com_v1_wordpress.yaml │ │ ├── example.com_v1alpha1_busybox.yaml │ │ ├── example.com_v1alpha1_memcached.yaml │ │ ├── example.com_v2_wordpress.yaml │ │ └── kustomization.yaml │ └── webhook/ │ ├── kustomization.yaml │ ├── manifests.yaml │ └── service.yaml ├── dist/ │ ├── chart/ │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates/ │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── cert-manager/ │ │ │ │ ├── metrics-certs.yaml │ │ │ │ ├── selfsigned-issuer.yaml │ │ │ │ └── serving-cert.yaml │ │ │ ├── crd/ │ │ │ │ ├── busyboxes.example.com.testproject.org.yaml │ │ │ │ ├── memcacheds.example.com.testproject.org.yaml │ │ │ │ └── wordpresses.example.com.testproject.org.yaml │ │ │ ├── manager/ │ │ │ │ └── manager.yaml │ │ │ ├── metrics/ │ │ │ │ └── controller-manager-metrics-service.yaml │ │ │ ├── monitoring/ │ │ │ │ └── servicemonitor.yaml │ │ │ ├── rbac/ │ │ │ │ ├── busybox-admin-role.yaml │ │ │ │ ├── busybox-editor-role.yaml │ │ │ │ ├── busybox-viewer-role.yaml │ │ │ │ ├── controller-manager.yaml │ │ │ │ ├── leader-election-role.yaml │ │ │ │ ├── leader-election-rolebinding.yaml │ │ │ │ ├── manager-role.yaml │ │ │ │ ├── manager-rolebinding.yaml │ │ │ │ ├── memcached-admin-role.yaml │ │ │ │ ├── memcached-editor-role.yaml │ │ │ │ ├── memcached-viewer-role.yaml │ │ │ │ ├── metrics-auth-role.yaml │ │ │ │ ├── metrics-auth-rolebinding.yaml │ │ │ │ ├── metrics-reader.yaml │ │ │ │ ├── wordpress-admin-role.yaml │ │ │ │ ├── wordpress-editor-role.yaml │ │ │ │ └── wordpress-viewer-role.yaml │ │ │ └── webhook/ │ │ │ ├── validating-webhook-configuration.yaml │ │ │ └── webhook-service.yaml │ │ └── values.yaml │ └── install.yaml ├── go.mod ├── grafana/ │ ├── controller-resources-metrics.json │ ├── controller-runtime-metrics.json │ └── custom-metrics/ │ └── config.yaml ├── hack/ │ └── boilerplate.go.txt ├── internal/ │ ├── controller/ │ │ ├── busybox_controller.go │ │ ├── busybox_controller_test.go │ │ ├── memcached_controller.go │ │ ├── memcached_controller_test.go │ │ ├── suite_test.go │ │ ├── wordpress_controller.go │ │ └── wordpress_controller_test.go │ └── webhook/ │ ├── v1/ │ │ ├── webhook_suite_test.go │ │ ├── wordpress_webhook.go │ │ └── wordpress_webhook_test.go │ └── v1alpha1/ │ ├── memcached_webhook.go │ ├── memcached_webhook_test.go │ └── webhook_suite_test.go └── test/ ├── e2e/ │ ├── e2e_suite_test.go │ └── e2e_test.go └── utils/ └── utils.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yaml ================================================ name: Bug Report description: Problems and issues with code or docs labels: - kind/bug body: - type: markdown attributes: value: | :warning: **Stop!** :warning: * If this is an issue with some sort of **runtime mechanics**, it probably belongs in [controller-runtime][cr-issue] instead * If this is an issue with **CRD generation**, webhook config generation, or deepcopy generation, it probably belongs in [controller-tools][ct-issue]. * If this is an issue with **scaffolding**, or is definitely a cross-repository effort, it probably belongs here. Feel free to continue :wink: [cr-issue]: https://github.com/kubernetes-sigs/controller-runtime/issues/new [ct-issue]: https://github.com/kubernetes-sigs/controller-tools/issues/new - type: markdown attributes: value: | # Hiya! Welcome to Kubebuilder! For a smooth issue process, try to answer the following questions. Don't worry if they're not all applicable; just try to include what you can :smile: If you need to include code snippets or logs, please put them in fenced code blocks, and if they're really long, use the [`
` tag][mdn-details], like:
Code & details examples `````markdown Some code written in Go: ```go type Manager struct { // FixTheBug removes all bugs from controller-runtime FixTheBug bool } ```
Some really long logs ``` ok ok ok SHOOT A BUG HAPPENS HERE OH NO ok ok done ```
````` [mdn-details]: ://developer.mozilla.org/en-US/docs/Web/HTML/Element/details - type: textarea attributes: label: What broke? What's expected? description: | Describe what didn't go the way you thought it would. Please include *full* & *exact* error messages if you can! If you have an idea of what went wrong, feel free to include that as well. validations: {required: true} - type: textarea attributes: label: Reproducing this issue description: If you have simple reproduction steps, or a minimal reproducer code snippet, please include it here. If they're already described above, no need to duplicate it here :smile:. - type: markdown attributes: value: | ## What versions are you using? Please specify the relevant versions and sources for the pieces of kubebuilder that you're using. The more details you can provide, the better. - type: input id: cli-version attributes: label: KubeBuilder (CLI) Version description: "use `kubebuilder version` to find this out" validations: required: true # project-version & plugin versions are not required for issues with initial scaffolding - type: input id: project-version attributes: label: PROJECT version description: "look for the `version` field in your PROJECT file to find this" - type: textarea attributes: label: Plugin versions description: "list the values of the `layout` field in your PROJECT file, if on KubeBuilder v3+" render: yaml - type: textarea attributes: label: Other versions description: | Often times, the following pieces of information are relevant: - Go version (`go version`) - controller-runtime & controller-tools version (check your `go.mod` file) - Kubernetes & kubectl versions (run `kubectl version` against your api server) - type: dropdown attributes: label: "Extra Labels" description: | If this is *also* a documentation request, etc, please select that below. multiple: true options: - "/kind documentation" - "/kind feature" - "/kind regression" - "/kind deprecation" - "/kind cleanup" ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ # allow free form issues as an escape hatch. This can be taken away if people abuse it ;-) blank_issues_enabled: true # link to CR and CT for easier access contact_links: - name: Runtime Issues url: https://github.com/kubernetes-sigs/controller-runtime/issues/new about: Runtime issues generally belong in the controller-runtime repository - name: CRD/Webhook/Deepcopy Generation Issues url: https://github.com/kubernetes-sigs/controller-tools/issues/new about: YAML & Go generation issues generally belong in the controller-tools repository - name: Support Questions url: https://github.com/kubernetes-sigs/kubebuilder/discussions/new about: Need support & not sure if this a bug? You can ask questions in Slack or GitHub discussions. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yaml ================================================ name: Feature request description: Suggest an idea for this project or its docs labels: - kind/feature body: - type: markdown attributes: value: | :warning: **Stop!** :warning: * If this is an issue with some sort of **runtime mechanics**, it probably belongs in [controller-runtime][cr-issue] instead * If this is an issue with **CRD generation**, webhook config generation, or deepcopy generation, it probably belongs in [controller-tools][ct-issue]. * If this is an issue with **scaffolding**, or is definitely a cross-repository effort, it probably belongs here. Feel free to continue :wink: [cr-issue]: https://github.com/kubernetes-sigs/controller-runtime/issues/new [ct-issue]: https://github.com/kubernetes-sigs/controller-tools/issues/new - type: markdown attributes: value: | # Hiya! Welcome to Kubebuilder! For a smooth issue process, try to answer the following questions. Don't worry if they're not all applicable; just try to include what you can :smile: If you need to include code snippets or logs, please put them in fenced code blocks, and if they're really long, use the [`
` tag][mdn-details], like:
Code & details examples `````markdown Some code written in Go: ```go type Manager struct { // FixTheBug removes all bugs from controller-runtime FixTheBug bool } ```
Some really long logs ``` ok ok ok SHOOT A BUG HAPPENS HERE OH NO ok ok done ```
````` [mdn-details]: ://developer.mozilla.org/en-US/docs/Web/HTML/Element/details - type: textarea attributes: label: What do you want to happen? description: | Describe the feature you want, and what the motivation is. What are your use cases? Why should we do this? Does it require a particular Kubernetes version? Is there currently another issue associated with this (use github syntax like `#xyz` to link to it)? validations: {required: true} - type: dropdown attributes: label: "Extra Labels" description: | If this is *also* a documentation request, etc, please select that below. multiple: true options: - "/kind documentation" - "/kind regression" - "/kind deprecation" - "/kind cleanup" ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy ## Security Announcements Join the [kubernetes-security-announce] group for security and vulnerability announcements related to the Kubernetes ecosystem. You can also subscribe to an RSS feed of these announcements using [this link][kubernetes-security-announce-rss]. ## Reporting a Vulnerability Instructions for reporting a vulnerability can be found on the [Kubernetes Security and Disclosure Information] page. ## Supported Versions Kubebuilder is tested against the latest three Kubernetes releases, in alignment with the [Kubernetes version and version skew support policy](https://kubernetes.io/docs/setup/release/version-skew-policy/). However, each version is only tested with the dependencies used for its release. For detailed information, please refer to the [compatibility and support policy on GitHub][compatibility-policy]. ## Release Policy Kubebuilder maintains a policy of releasing updates for the latest CLI version (currently v4). Older versions (v1, v2, v3) are no longer supported, and no releases will be produced for them. It is recommended to ensure that any project scaffolded by Kubebuilder remains aligned with the latest release. ## Automated Vulnerability Scanning Kubebuilder employs automated scanning via Dependabot and GitHub Actions within its CI/CD pipeline. This process detects vulnerabilities in dependencies and configurations, generating daily or weekly reports prioritized for the latest supported versions. - **Dependabot Configuration**: You can review the setup in `.github/dependabot.yml`. - **Security Checks**: Security checks are enabled in the Kubebuilder repository settings. - **Code Scanning**: The `.github/workflows/codeql.yml` workflow scans the `master` and `book-v4` branches, which typically contain the latest release code. Other release branches may not be scanned. ## Production-Grade Security Projects generated by Kubebuilder are designed for ease of development and are **not** configured with production-grade security settings. For example, default configurations do not enable cert-manager or perform proper certificate validation, which may not be suitable for production environments. Ensure that you make the necessary adjustments to security settings before releasing your solution for production. [kubernetes-security-announce]: https://groups.google.com/forum/#!forum/kubernetes-security-announce [kubernetes-security-announce-rss]: https://groups.google.com/forum/feed/kubernetes-security-announce/msgs/rss_v2_0.xml?num=50 [Kubernetes version and version skew support policy]: https://kubernetes.io/docs/setup/release/version-skew-policy/#supported-versions [Kubernetes Security and Disclosure Information]: https://kubernetes.io/docs/reference/issues-security/security/#report-a-vulnerability [compatibility-policy]: ./../README.md#versions-compatibility-and-supportability [project-upgrade-assistant]: https://book.kubebuilder.io/reference/rescaffold [testdata-directory]: https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata [kubebuilder-releases]: https://github.com/kubernetes-sigs/kubebuilder/releases ================================================ FILE: .github/dependabot.yml ================================================ # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" # Workflow files stored in the # default location of `.github/workflows` directory: "/" schedule: interval: "daily" commit-message: prefix: ":seedling:" labels: - "ok-to-test" # Maintain dependencies for go - package-ecosystem: "gomod" directory: "/" schedule: interval: "daily" commit-message: prefix: ":seedling:" labels: - "ok-to-test" # Maintain dependencies for go - package-ecosystem: "gomod" directory: "/testdata/project-v4" schedule: interval: "daily" commit-message: prefix: ":seedling:" labels: - "ok-to-test" # Maintain dependencies for dockerfile scaffold in the projects - package-ecosystem: docker directory: "testdata/project-v4" schedule: interval: daily commit-message: prefix: ":seedling:" # Maintain dependencies for go in external plugin sample - package-ecosystem: "gomod" directory: "docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1" schedule: interval: "daily" commit-message: prefix: ":book:" labels: - "ok-to-test" ================================================ FILE: .github/instructions/kubebuilder.instructions.md ================================================ See [AGENTS.md](../../AGENTS.md) for AI agent guidelines. ================================================ FILE: .github/workflows/apidiff.yml ================================================ name: APIDiff on: push: paths-ignore: - '**/*.md' pull_request: paths-ignore: - '**/*.md' jobs: go-apidiff: name: Verify API differences runs-on: ubuntu-latest # Pull requests from different repository only trigger this checks if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) steps: - name: Clone the code uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Execute go-apidiff uses: joelanford/go-apidiff@v0.8.3 with: compare-imports: true print-compatible: true ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "CodeQL Advanced" on: # We are checking both `master` and `book-v4` branches: # - `master` represents the latest development work. # - `book-v4` is the latest stable release branch, which contains the latest published code, # ensuring that any issues in production are identified and addressed promptly. schedule: - cron: '30 20 * * 1' # Runs every Monday at 8:30 PM jobs: analyze: name: Analyze Go runs-on: ubuntu-latest permissions: security-events: write packages: read actions: read contents: read steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Build and install Kubebuilder CLI run: make install # Preparing the project-v4 sample for CodeQL analysis: # - `go mod tidy` ensures dependencies are fully resolved. # - `make manifests` generates required manifests for a complete project structure. # - `make build` builds the project code, ensuring all components are ready for CodeQL analysis. - name: Build project-v4 sample project run: | cd testdata/project-v4 go mod tidy echo 'Running build commands for Go in project-v4' make manifests make build - name: Initialize CodeQL uses: github/codeql-action/init@v4 with: languages: go build-mode: autobuild - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v4 with: category: "/language:go" ================================================ FILE: .github/workflows/coverage.yml ================================================ name: Coverage on: push: paths-ignore: ['**/*.md'] pull_request: paths-ignore: ['**/*.md'] jobs: coverage: runs-on: ubuntu-latest if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) steps: - name: Checkout uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Remove pre-installed kustomize run: sudo rm -f /usr/local/bin/kustomize - name: Install Kubebuilder run: make install - name: Run tests with coverage run: make test-coverage - name: Upload coverage to Coveralls uses: shogo82148/actions-goveralls@v1 with: path-to-profile: coverage-all.out ================================================ FILE: .github/workflows/cross-platform-tests.yml ================================================ name: Cross-Platform Tests # Trigger the workflow on pull requests and direct pushes to any branch on: push: paths-ignore: - '**/*.md' pull_request: paths-ignore: - '**/*.md' jobs: test: name: ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: matrix: os: - ubuntu-latest - macos-latest # Pull requests from the same repository won't trigger this checks as they were already triggered by the push if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) steps: - name: Clone the code uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod # This step is needed as the following one tries to remove # kustomize for each test but has no permission to do so - name: Remove pre-installed kustomize run: sudo rm -f /usr/local/bin/kustomize - name: Unit Tests run: make test-unit - name: Run Testdata run: make test-testdata ================================================ FILE: .github/workflows/external-plugin.yml ================================================ name: External Plugin on: push: paths: - 'pkg/' - 'docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin' - '.github/workflows/external-plugin.yml' pull_request: paths: - 'pkg/' - 'docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin' - '.github/workflows/external-plugin.yml' jobs: external: name: Verify external plugin runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run tests run: make test-external-plugin ================================================ FILE: .github/workflows/legacy-webhook-path.yml ================================================ # This test ensure that the legacy webhook path # still working. The option is deprecated # and should be removed when we no longer need # to support go/v4 plugin. name: Legacy Webhook Path on: push: paths: - 'testdata/**' - '.github/workflows/legacy-webhook-path.yml' pull_request: paths: - 'testdata/**' - '.github/workflows/legacy-webhook-path.yml' jobs: webhook-legacy-path: name: Verify Legacy Webhook Path runs-on: ubuntu-latest # Pull requests from the same repository won't trigger this checks as they were already triggered by the push if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Clone the code uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run make test-legacy run: make test-legacy ================================================ FILE: .github/workflows/lint-sample.yml ================================================ name: Lint Samples on: push: paths: - 'testdata/**' - 'docs/book/src/**/testdata/**' - '.github/workflows/lint-sample.yml' pull_request: paths: - 'testdata/**' - 'docs/book/src/**/testdata/**' - '.github/workflows/lint-sample.yml' jobs: lint-samples: runs-on: ubuntu-latest strategy: fail-fast: false matrix: folder: [ "testdata/project-v4", "testdata/project-v4-with-plugins", "testdata/project-v4-multigroup", "docs/book/src/cronjob-tutorial/testdata/project", "docs/book/src/getting-started/testdata/project", "docs/book/src/multiversion-tutorial/testdata/project" ] if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) steps: - name: Clone the code uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Prepare ${{ matrix.folder }} working-directory: ${{ matrix.folder }} run: go mod tidy - name: Check linter configuration working-directory: ${{ matrix.folder }} run: make lint-config - name: Run linter uses: golangci/golangci-lint-action@v9 with: version: v2.8.0 working-directory: ${{ matrix.folder }} - name: Run linter via makefile target working-directory: ${{ matrix.folder }} run: make lint ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: push: paths-ignore: - '**/*.md' pull_request: paths-ignore: - '**/*.md' jobs: lint: name: golangci-lint runs-on: ubuntu-latest # Pull requests from the same repository won't trigger this checks as they were already triggered by the push if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) steps: - name: Clone the code uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Check linter configuration run: make lint-config - name: Run linter uses: golangci/golangci-lint-action@v9 with: version: v2.8.0 yamllint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6.0.2 - name: Install Helm run: make install-helm - name: Run yamllint (YAML + Helm chart output 2-space indentation) run: make yamllint - name: Check sample permissions run: make check-sample-permissions license: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6.0.2 - name: Run license check run: make test-license ================================================ FILE: .github/workflows/release-version-ci.yml ================================================ name: Test GoReleaser and CLI Version on: pull_request: branches: - master paths: - 'pkg/**' - 'cmd/**' - 'build/.goreleaser.yml' - '.github/workflows/release-version-ci.yml' jobs: go-releaser-test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Clean dist directory run: rm -rf dist || true - name: Create temporary git tag run: | git tag v4.5.3-rc.1 - name: Install Syft to generate SBOMs run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b $HOME/bin echo "$HOME/bin" >> $GITHUB_PATH - name: Run GoReleaser in mock mode using tag uses: goreleaser/goreleaser-action@v7 with: version: v2.7.0 args: release --skip=publish --clean -f ./build/.goreleaser.yml - name: Init project using built kubebuilder binary and check cliVersion run: | mkdir test-operator && cd test-operator go mod init test-operator chmod +x ../dist/kubebuilder_linux_$(go env GOARCH)_v1/kubebuilder ../dist/kubebuilder_linux_$(go env GOARCH)_v1/kubebuilder init --domain example.com echo "PROJECT file content:" cat PROJECT echo "Verifying cliVersion value..." grep '^cliVersion: 4.5.3-rc.1$' PROJECT ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: push: tags: - '*' permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6.0.2 with: fetch-depth: 0 - name: Fetch all tags run: git fetch --force --tags - name: Set up Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Clean dist directory run: rm -rf dist || true - name: Install Syft to generate SBOMs run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b $HOME/bin echo "$HOME/bin" >> $GITHUB_PATH - name: Run GoReleaser uses: goreleaser/goreleaser-action@v7 with: version: v2.7.0 args: release -f ./build/.goreleaser.yml env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Upload assets uses: actions/upload-artifact@v7.0.0 with: name: kubebuilder path: dist/* if-no-files-found: error ================================================ FILE: .github/workflows/scorecard.yml ================================================ name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: "20 15 * * 5" push: branches: ["master"] # Declare default permissions as read only. permissions: read-all jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest # `publish_results: true` only works when run from the default branch. conditional can be removed if disabled. if: github.event.repository.default_branch == github.ref_name || github.event_name == 'pull_request' permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Uncomment the permissions below if installing in a private repository. # contents: read # actions: read steps: - name: "Checkout code" uses: actions/checkout@0c366fd6a839edf440554fa01a7085ccba70ac98 # v6.0.1 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # (Optional) Uncomment file_mode if you have a .gitattributes with files marked export-ignore # file_mode: git # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@v4 with: sarif_file: results.sarif ================================================ FILE: .github/workflows/spaces.yml ================================================ name: Trailing on: push: paths: - '**/*.md' pull_request: paths: - '**/*.md' jobs: lint: name: "Check Trailing" runs-on: ubuntu-latest # Pull requests from the same repository won't trigger this checks as they were already triggered by the push if: (github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository) steps: - name: Clone the code uses: actions/checkout@v6.0.2 - name: Run check run: make test-spaces ================================================ FILE: .github/workflows/test-alpha-generate.yml ================================================ name: Test Alpha Generate on: push: paths: - 'pkg/cli/alpha/**' - '.github/workflows/test-alpha-generate.yml' pull_request: paths: - 'pkg/cli/alpha/**' - '.github/workflows/test-alpha-generate.yml' jobs: unsupported: runs-on: ubuntu-latest strategy: fail-fast: true if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install dependencies and generate binary run: make install - name: Navigate to testdata/project-v4 run: cd testdata/project-v4 - name: Update PROJECT file run: | sed -i 's#go.kubebuilder.io/v4#go.kubebuilder.io/v3#g' testdata/project-v4/PROJECT # Validate if help output is working and workaround to # update the PROJECT file in memory to allow upgrade # no longer supported layouts did not break the command options - name: Validate help output for alpha generate run: | if kubebuilder alpha generate --help | grep -q "kubebuilder alpha generate \[flags\]"; then echo "Help output validated" else echo "Help output missing or invalid" exit 1 fi - name: Run kubebuilder alpha generate run: | cd testdata/project-v4 && kubebuilder alpha generate ================================================ FILE: .github/workflows/test-book.yml ================================================ name: E2E Book Samples on: push: paths: - 'docs/book/src/getting-started/testdata/project/**' - 'docs/book/src/cronjob-tutorial/testdata/project/**' - 'docs/book/src/multiversion-tutorial/testdata/project/**' - '.github/workflows/test-e2e-book.yml' pull_request: paths: - 'docs/book/src/getting-started/testdata/project/**' - 'docs/book/src/cronjob-tutorial/testdata/project/**' - 'docs/book/src/multiversion-tutorial/testdata/project/**' - '.github/workflows/test-e2e-book.yml' jobs: e2e: runs-on: ubuntu-latest strategy: fail-fast: true matrix: folder: [ "docs/book/src/getting-started/testdata/project", "docs/book/src/cronjob-tutorial/testdata/project", "docs/book/src/multiversion-tutorial/testdata/project" ] if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Running make test for ${{ matrix.folder }} working-directory: ${{ matrix.folder }} run: make test - name: Running make test-e2e for ${{ matrix.folder }} working-directory: ${{ matrix.folder }} run: make test-e2e ================================================ FILE: .github/workflows/test-devcontainer.yml ================================================ name: Test DevContainer Image on: push: paths: - 'testdata/**' - '.github/workflows/test-devcontainer.yml' - 'pkg/plugins/golang/v4/scaffolds/internal/templates/devcontainer.go' pull_request: paths: - 'testdata/**' - '.github/workflows/test-devcontainer.yml' - 'pkg/plugins/golang/v4/scaffolds/internal/templates/devcontainer.go' jobs: test-devcontainer: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Build and Run Dev Container Tests uses: devcontainers/ci@v0.3 with: subFolder: testdata/project-v4 runCmd: | # Source bash-completion for all tests source /usr/share/bash-completion/bash_completion # Verify Tools Installation echo "Verifying installed tools..." docker --version kind version kubebuilder version kubectl version --client go version echo "All required tools are installed" # Verify common-utils feature is installed echo "Verifying common-utils feature..." if ! command -v zsh &> /dev/null; then echo "ERROR: common-utils feature not installed (zsh missing)" exit 1 fi echo "common-utils feature is installed" # Verify bash-completion setup echo "Verifying bash-completion configuration..." if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc; then echo "ERROR: bash-completion not configured in .bashrc" exit 1 fi echo "bash-completion is configured in .bashrc" # Verify completion files exist echo "Verifying completion files..." for cmd in kubectl kind kubebuilder docker; do if [ ! -f "/usr/share/bash-completion/completions/${cmd}" ]; then echo "ERROR: Completion file for ${cmd} not found" exit 1 fi echo "${cmd} completion file exists" done echo "All completion files are present" # Test bash-completion works echo "Testing bash-completion functionality..." # Bash-completion uses lazy-loading, so manually source completion to test source /usr/share/bash-completion/completions/kubectl if ! complete -p kubectl &> /dev/null; then echo "ERROR: kubectl completions not loaded" exit 1 fi echo "Bash completions are working" # Test Docker-in-Docker echo "Testing Docker-in-Docker functionality..." docker ps docker network ls docker run --rm hello-world echo "Docker is working inside devcontainer" # Test Kubebuilder Workflow echo "Testing Kubebuilder development workflow..." go mod tidy make all echo "make all passed" make docker-build IMG=controller:ci echo "make docker-build passed" # Test Kind Cluster Creation echo "Testing kind cluster creation..." kind create cluster --name test-cluster kubectl cluster-info --context kind-test-cluster kubectl get nodes kind delete cluster --name test-cluster echo "Kind cluster creation and deletion successful" ================================================ FILE: .github/workflows/test-e2e-samples.yml ================================================ name: E2E Testdata Sample on: push: paths: - 'testdata/**' - '.github/workflows/test-e2e-samples.yml' pull_request: paths: - 'testdata/**' - '.github/workflows/test-e2e-samples.yml' jobs: e2e-tests-project-v4: runs-on: ubuntu-latest strategy: fail-fast: true if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Prepare project-v4 run: | # Enable [METRICS-WITH-CERTS] by uncommenting the lines in kustomization.yaml KUSTOMIZATION_FILE_PATH="testdata/project-v4/config/default/kustomization.yaml" sed -i '47,49s/^#//' $KUSTOMIZATION_FILE_PATH cd testdata/project-v4/ go mod tidy - name: Testing make test-e2e for project-v4 working-directory: testdata/project-v4/ run: | make test-e2e e2e-tests-project-v4-with-plugins: runs-on: ubuntu-latest strategy: fail-fast: true if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Prepare project-v4-with-plugins run: | cd testdata/project-v4-with-plugins/ go mod tidy - name: Testing make test-e2e for project-v4-with-plugins working-directory: testdata/project-v4-with-plugins/ run: | make test-e2e e2e-tests-project-v4-multigroup: runs-on: ubuntu-latest strategy: fail-fast: true if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Prepare project-v4-multigroup run: | cd testdata/project-v4-multigroup go mod tidy - name: Testing make test-e2e for project-v4-multigroup working-directory: testdata/project-v4-multigroup/ run: | make test-e2e # Test to validate e2e integration when no APIs are scaffolded e2e-test-basic-project: runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Build kubebuilder CLI from this repo run: make install - name: Scaffold empty go/v4 project run: | mkdir -p /tmp/basic-project-v4 cd /tmp/basic-project-v4 kubebuilder init --plugins go/v4 --domain example.com --repo example.com/empty-operator go mod tidy make - name: Run make test-e2e on empty project working-directory: /tmp/basic-project-v4 run: make test-e2e ================================================ FILE: .github/workflows/test-helm-book.yml ================================================ name: Helm Docs Tutorials on: push: paths: - "docs/book/src/cronjob-tutorial/testdata/project/**" - "docs/book/src/getting-started/testdata/project/**" - "docs/book/src/multiversion-tutorial/testdata/project/**" - ".github/workflows/test-helm-book.yml" pull_request: paths: - "docs/book/src/cronjob-tutorial/testdata/project/** " - "docs/book/src/getting-started/testdata/project/**" - "docs/book/src/multiversion-tutorial/testdata/project/**" - ".github/workflows/test-helm-book.yml" jobs: helm-test: runs-on: ubuntu-latest strategy: fail-fast: true matrix: folder: [ "docs/book/src/getting-started/testdata/project", "docs/book/src/cronjob-tutorial/testdata/project", "docs/book/src/multiversion-tutorial/testdata/project" ] if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Set project name id: project run: echo "name=$(basename ${{ matrix.folder }})" >> $GITHUB_OUTPUT - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Prepare project run: | cd ${{ matrix.folder }} go mod tidy make docker-build IMG=${{ steps.project.outputs.name}}:v0.1.0 kind load docker-image ${{ steps.project.outputs.name}}:v0.1.0 - name: Install Helm run: make install-helm - name: Lint Helm chart run: | helm lint ${{ matrix.folder }}/dist/chart - name: Install Prometheus Operator CRDs run: | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm install prometheus-crds prometheus-community/prometheus-operator-crds - name: Install cert-manager via Helm run: | helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true - name: Wait for cert-manager to be ready run: | kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook - name: Render Helm chart run: | helm template ${{ matrix.folder }}/dist/chart --namespace=${{ steps.project.outputs.name }}-system - name: Deploy manager via Helm working-directory: ${{ matrix.folder }} run: | make helm-deploy IMG=${{ steps.project.outputs.name}}:v0.1.0 - name: Check Helm release status working-directory: ${{ matrix.folder }} run: | make helm-status - name: Run Helm tests run: | helm test ${{ steps.project.outputs.name}} --namespace ${{ steps.project.outputs.name}}-system ================================================ FILE: .github/workflows/test-helm-samples.yml ================================================ name: Helm Testdata Sample on: push: paths: - "testdata/project-v4-with-plugins/**" - ".github/workflows/test-helm-samples.yml" pull_request: paths: - "testdata/project-v4-with-plugins/**" - ".github/workflows/test-helm-samples.yml" jobs: helm-test-project-v4-with-plugins: runs-on: ubuntu-latest strategy: fail-fast: true if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Enable Prometheus in kustomize (testdata sample) run: | sed -i 's/^#- \.\.\/prometheus/- ..\/prometheus/' testdata/project-v4-with-plugins/config/default/kustomization.yaml - name: Build kubebuilder CLI run: make build - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Prepare project-v4-with-plugins run: | cd testdata/project-v4-with-plugins/ go mod tidy make all - name: Rebuild installer and regenerate Helm chart (v2-alpha) working-directory: testdata/project-v4-with-plugins run: | make build-installer ../../bin/kubebuilder edit --plugins=helm/v2-alpha --force - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Install Helm run: make install-helm - name: Lint Helm chart for project-v4-with-plugins run: | helm lint testdata/project-v4-with-plugins/dist/chart - name: Build project-v4-with-plugins run: | cd testdata/project-v4-with-plugins/ go mod tidy make docker-build IMG=controller:latest kind load docker-image controller:latest - name: Install Prometheus Operator CRDs run: | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm install prometheus-crds prometheus-community/prometheus-operator-crds - name: Install cert-manager via Helm (wait for readiness) run: | helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true \ --wait \ --timeout 300s - name: Render Helm chart for project-v4-with-plugins run: | helm template testdata/project-v4-with-plugins/dist/chart --namespace=project-v4-with-plugins-system - name: Deploy manager via Helm working-directory: testdata/project-v4-with-plugins run: | make helm-deploy IMG=controller:latest HELM_EXTRA_ARGS="--set prometheus.enable=true" - name: Check Helm release status working-directory: testdata/project-v4-with-plugins run: | make helm-status - name: Run Helm tests run: | helm test project-v4-with-plugins --namespace project-v4-with-plugins-system - name: Delete kind cluster if: always() run: | kind delete cluster || true # Test scenario: # - scaffold project without creating webhooks, # - deploy helm chart without installing cert manager; # - check that deployment has been deployed; # # Command to use to scaffold project without creating webhooks and so no need to install cert manager: # - kubebuilder init # - kubebuilder create api --group example.com --version v1 --kind App --controller=true --resource=true # - kubebuilder edit --plugins=helm.kubebuilder.io/v2-alpha test-helm-no-webhooks: runs-on: ubuntu-latest if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Checkout repository uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Create kind cluster run: kind create cluster - name: Install Helm run: make install-helm - name: Install kubebuilder binary run: make install - name: Create test directory run: mkdir -p test-helm-no-webhooks - name: Scaffold project with kubebuilder commands working-directory: test-helm-no-webhooks run: | go mod init test-helm-no-webhooks kubebuilder init kubebuilder create api --group example.com --version v1 --kind App --controller=true --resource=true kubebuilder edit --plugins=helm.kubebuilder.io/v2-alpha - name: Build and load Docker image working-directory: test-helm-no-webhooks run: | make docker-build IMG=controller:latest kind load docker-image controller:latest - name: Lint Helm chart working-directory: test-helm-no-webhooks run: helm lint ./dist/chart - name: Deploy manager via Helm working-directory: test-helm-no-webhooks run: | make helm-deploy IMG=controller:latest - name: Verify deployment is working working-directory: test-helm-no-webhooks run: | make helm-status - name: Run Helm tests working-directory: test-helm-no-webhooks run: | helm test test-helm-no-webhooks --namespace test-helm-no-webhooks-system - name: Delete kind cluster if: always() run: | kind delete cluster || true ================================================ FILE: .github/workflows/testdata.yml ================================================ name: Testdata verification on: push: pull_request: jobs: testdata: name: Verify testdata directory runs-on: ubuntu-latest # Pull requests from the same repository won't trigger this checks as they were already triggered by the push if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository steps: - name: Clone the code uses: actions/checkout@v6.0.2 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Remove pre-installed kustomize # This step is needed as the following one tries to remove # kustomize for each test but has no permission to do so run: sudo rm -f /usr/local/bin/kustomize - name: Verify testdata directory run: make check-testdata - name: Verify docs update run: make check-docs ================================================ FILE: .github/workflows/verify.yml ================================================ name: "PR Title Verifier" on: pull_request: types: [opened, edited, synchronize, reopened] jobs: verify: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6.0.2 - name: Validate PR Title Format env: TITLE: ${{ github.event.pull_request.title }} run: | if [[ -z "$TITLE" ]]; then echo "Error: PR title cannot be empty." exit 1 fi if ! [[ "$TITLE" =~ ^($'\u26A0'|$'\u2728'|$'\U0001F41B'|$'\U0001F4D6'|$'\U0001F680'|$'\U0001F331') ]]; then echo "Error: Invalid PR title format." echo "Your PR title must start with one of the following indicators:" echo "- Breaking change: ⚠ (U+26A0)" echo "- Non-breaking feature: ✨ (U+2728)" echo "- Patch fix: 🐛 (U+1F41B)" echo "- Docs: 📖 (U+1F4D6)" echo "- Release: 🚀 (U+1F680)" echo "- Infra/Tests/Other: 🌱 (U+1F331)" exit 1 fi echo "PR title is valid: '$TITLE'" ================================================ FILE: .gitignore ================================================ .idea/ .vscode/ WORKSPACE .DS_Store # don't check in the build output of the book docs/book/book/ # ignore auto-generated dir by `mdbook serve` docs/book/src/docs # Editor temp files *~ \#*# *.swp # skip bin dirs **/bin **/testbin # skip GoReleaser dist directory (root level only, not testdata) /dist # skip .out files (coverage tests) *.out # skip testdata go.sum, since it may have # different result depending on go version /testdata/**/go.sum /docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1/bin /testdata/**legacy** ## Skip testdata files that generate by tests using TestContext **/e2e-*/** # Optional rendered chart output (e.g. from make yamllint-helm when debugging) testdata/.helm-rendered.yaml ================================================ FILE: .golangci.yml ================================================ version: "2" run: allow-parallel-runners: true linters: default: none enable: - asciicheck - bidichk - copyloopvar - depguard - dupl - errcheck - ginkgolinter - goconst - gocyclo - govet - importas - ineffassign - lll - modernize - misspell - nakedret - nolintlint - prealloc - revive - staticcheck - unconvert - unparam - unused - wrapcheck - whitespace settings: depguard: rules: forbid-pkg-errors: deny: - pkg: sort desc: Should be replaced with slices package ginkgolinter: forbid-focus-container: true forbid-spec-pollution: true govet: disable: - fieldalignment enable-all: true importas: no-unaliased: true alias: - pkg: sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha alias: helmv1alpha - pkg: sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha alias: helmv2alpha - pkg: sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha alias: grafanav1alpha - pkg: "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha" alias: autoupdatev1alpha - pkg: sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1 alias: deployimagev1alpha1 - pkg: sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4 alias: golangv4 - pkg: sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2 alias: kustomizecommonv2 modernize: disable: # Suggest replacing omitempty with omitzero for struct fields. # Disable this check for now since it introduces too many changes in our existing codebase. # See https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#hdr-Analyzer_omitzero for more details. - omitzero nolintlint: allow-unused: false revive: rules: - name: blank-imports - name: context-as-argument - name: context-keys-type - name: dot-imports arguments: - allowedPackages: - github.com/onsi/ginkgo/v2 - github.com/onsi/gomega - name: error-return - name: error-strings - name: error-naming - name: exported disabled: true - name: if-return - name: import-shadowing - name: increment-decrement - name: var-naming severity: warning arguments: - ["ID"] # allowed initialisms - ["VM"] # disallowed initialisms - [ # <-- this is a list containing one map { skip-initialism-name-checks: true, upper-case-const: true, skip-package-name-checks: true, extra-bad-package-names: ["helpers", "models"], }, ] - name: var-declaration - name: package-comments disabled: true - name: range - name: receiver-naming - name: time-naming - name: unexported-return - name: indent-error-flow - name: errorf - name: empty-block - name: superfluous-else - name: unused-parameter - name: unreachable-code - name: redefines-builtin-id - name: bool-literal-in-expr - name: constant-logical-expr - name: comment-spacings exclusions: generated: lax rules: - linters: - gosec path: test/e2e/* - linters: - gosec - lll path: hack/docs/* paths: - third_party$ - builtin$ - examples$ formatters: enable: - gci - gofmt - gofumpt - goimports settings: gci: sections: - standard - default - prefix(sigs.k8s.io/kubebuilder) exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: .yamllint ================================================ # yamllint configuration for Kubebuilder # Aligns with Kubernetes YAML best practices: # - 2-space indentation, spaces only (no tabs) # - true/false over yes/no for booleans (truthy) # - Newline at end of file, no trailing spaces (POSIX) # - Unix newlines, consistent colons/hyphens # - No duplicate keys (key-duplicates) # - Document start (---) recommended for manifests; not enforced to allow existing testdata extends: default rules: line-length: max: 120 allow-non-breakable-words: true # Generated CRDs and Prometheus manifests often have long lines ignore: | **/config/crd/bases/*.yaml **/config/prometheus/*.yaml indentation: spaces: 2 # Kubernetes manifests often use list items at same indent as key (rules:\n- apiGroups:) indent-sequences: whatever # Uncommented replacement blocks in config/default/kustomization.yaml use 1-space for list items ignore: | **/config/default/kustomization.yaml # Kubernetes: prefer true/false over yes/no; do not check keys (e.g. "on" in workflows) truthy: check-keys: false # Allow single trailing blank line (common in editors) empty-lines: max-end: 1 # Optional: document-start (---) is K8s best practice but not enforced for testdata document-start: disable document-end: disable ================================================ FILE: .yamllint-helm ================================================ # yamllint config for Helm-rendered manifest output (stdin or temp files). # Extends repo .yamllint; relaxes rules that are noisy for generated/template output. extends: .yamllint rules: line-length: disable trailing-spaces: disable ================================================ FILE: AGENTS.md ================================================ # Kubebuilder AI Agent Guide **Kubebuilder** is a **framework** and **command-line interface (CLI)** for building **Kubernetes APIs** using **Custom Resource Definitions (CRDs)**. It provides scaffolding and abstractions that accelerate the development of **controllers**, **webhooks**, and **APIs** written in **Go**. ## Quick Reference | Item | Value | |------------|-----------------------------------------------------------| | Language | Defined in the go.mod | | Module | `sigs.k8s.io/kubebuilder/v4` | | Binary | `./bin/kubebuilder` | | Core deps | `controller-runtime`, `controller-tools`, Helm, Kustomize | | Docs | https://book.kubebuilder.io | ## Directory Map ``` pkg/ cli/ CLI command implementations alpha/ Alpha/experimental commands (generate, update, etc.) init.go 'init' command + default PluginBundle definition api.go 'create api' command webhook.go 'create webhook' command edit.go 'edit' command root.go Root command setup machinery/ Scaffolding engine (templates, markers, injectors) template.go Base template interface inserter.go Code injection engine marker.go Marker detection and processing filesystem.go Filesystem abstraction (uses afero) model/ resource/ Resource model (GVK, API, Controller, Webhook) stage/ Plugin execution stages plugin/ Plugin interfaces and utilities interface.go Core plugin interfaces (Plugin, Init, CreateAPI, etc.) bundle.go Plugin composition util/ Helper functions for plugin authors plugins/ Plugin implementations (ADD NEW PLUGINS HERE) golang/v4/ Main Go scaffolding (default for go projects) scaffolds/ Scaffolding for init, api, webhook internal/templates/ Template implementations golang/deployimage/ Deploy-image pattern plugin common/kustomize/v2/ Kustomize manifest generation (default) optional/ Optional plugins (enabled via --plugins flag) helm/ Helm chart generation (v1alpha deprecated, v2alpha current) grafana/ Grafana dashboard generation autoupdate/ Auto-update GitHub workflow external/ External plugin support (exec-based plugins) docs/book/ mdBook documentation (https://book.kubebuilder.io) src/ Markdown source files **/testdata/ Sample projects used in docs (regenerated) test/ e2e/ E2E tests requiring Kubernetes cluster v4/ Tests for v4 plugin helm/ Tests for Helm plugin deployimage/ Tests for deploy-image plugin utils/ Test helpers (TestContext, etc.) testdata/ Scripts to generate testdata projects generate.sh Main generation script test.sh Tests all testdata projects testdata/ Generated complete sample projects (DO NOT EDIT) project-v4/ Basic v4 project project-v4-multigroup/ Multigroup project project-v4-with-plugins/ Project with optional plugins hack/docs/ Documentation generation generate.sh Regenerate docs samples + marker docs generate_samples.go Sample generation logic cmd/ CLI entry point version.go Version info (updated by make update-k8s-version) main.go Application entry point ``` **Key Locations for Common Tasks:** - Add new plugin → `pkg/plugins///` - Add new template → `pkg/plugins//scaffolds/internal/templates/` - Modify CLI commands → `pkg/cli/` - Add scaffolding machinery → `pkg/machinery/` - Add tests → `test/e2e/all/plugin__test.go` or `pkg//*_test.go` ## Critical Rules ### Do Not Manually Edit Generated Files - `testdata/` - regenerated via `make generate-testdata` - `docs/book/**/testdata/` - regenerated via `make generate-docs` - `*/dist/chart/` - regenerated via `make generate-charts` ### File-Specific Requirements After making changes, run the appropriate commands based on what you modified: **Generate Commands (rebuild artifacts):** - **If you modify files in `hack/docs/internal/`** → run `make install && make generate-docs` - **If you modify files in `pkg/plugins/optional/helm/`** → run `make install && make generate-charts` - **If you modify any boilerplate/template files** → run `make install && make generate` **Formatting Commands:** - After editing `*.go` → `make lint-fix` - After editing `*.md` → `make remove-spaces` **Always Run Before PR:** ```bash make lint-fix # Auto-fix Go code style make test-unit # Verify unit tests pass ``` **Note:** Boilerplate/template files are Go files that define scaffolding templates, typically located in `pkg/plugins/**/scaffolds/internal/templates/` or files that generate code/configs for scaffolded projects. ## Development Workflow ### Build & Install ```bash make build # Build to ./bin/kubebuilder make install # Copy to $(go env GOBIN) ``` ### Lint & Format ```bash make lint # Check only (golangci-lint + yamllint) make lint-fix # Auto-fix Go code ``` ### Testing ```bash make test-unit # Fast unit tests (./pkg/..., ./test/e2e/utils/...) make test-integration # Integration tests (may create temp dirs, download binaries) make test-testdata # Test all testdata projects make test-e2e-local # Full e2e (creates kind cluster) make test # CI aggregate (all of above + license) ``` ## PR Submission ### PR Title Format (MANDATORY) PR titles use **emojis** (appear in release notes). Format: `:emoji: [(plugin/version)]: Description` The `(plugin/version)` scope is optional; omit it for repo-wide or documentation-only changes. **Emojis:** - ⚠️ (`:warning:`) - Breaking change - ✨ (`:sparkles:`) - New feature - 🐛 (`:bug:`) - Bug fix - 📖 (`:book:`) - Documentation - 🌱 (`:seedling:`) - Infrastructure/tests/refactor **Examples:** ``` 🐛 Resolve nil pointer panic in scaffold generator ✨ (helm/v2-alpha): Add cluster-scoped resource support 📖 (go/v4): Update deployment documentation ✨ Update dependencies to latest versions ``` ### Commit Message Format Commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. Format: `[optional scope]: ` The `[optional scope]` is typically the plugin/version (e.g., `helm/v2-alpha`, `go/v4`); omit it for repo-wide or non-plugin changes. **Types:** - **feat**: A new feature for the user or a plugin - **fix**: A bug fix for the user or a plugin - **docs**: Documentation changes only - **test**: Adding or updating tests - **refactor**: Code change that neither fixes a bug nor adds a feature - **chore**: Changes to build process, dependencies, or maintenance tasks - **breaking**: A breaking change (can be combined with other types) **Examples:** ``` fix: Resolve nil pointer panic in scaffold generator feat(helm/v2-alpha): Add cluster-scoped resource support docs(go/v4): Update deployment documentation chore: Update dependencies to latest versions ``` ### Pre-PR Checklist - [ ] One commit per PR (squash all) - [ ] Add/update tests for new behavior - [ ] Add/update docs for new behavior - [ ] Run `make lint-fix` - [ ] Run `make install` - [ ] Run `make generate` - [ ] Run `make test-unit` ## Core Concepts ### Plugin Architecture Plugins implement interfaces from `pkg/plugin/`: - `Plugin` - base interface (Name, Version, SupportedProjectVersions) - `Init` - project initialization (`kubebuilder init`) - `CreateAPI` - API creation (`kubebuilder create api`) - `CreateWebhook` - webhook creation (`kubebuilder create webhook`) - `Edit` - post-init modifications (`kubebuilder edit`) - `Bundle` - groups multiple plugins **Plugin Bundles:** Default bundle (`pkg/cli/init.go`): `go.kubebuilder.io/v4` + `kustomize.common.kubebuilder.io/v2` Plugins resolve via `pkg/plugin` registry and execute in order. **External Plugins:** Executable binaries in `pkg/plugins/external/` that communicate via JSON over stdin/stdout. ### Scaffolding Machinery From `pkg/machinery/`: - `Template` - file generation via Go templates - `Inserter` - code injection at markers - `Marker` - special comments (e.g., `// +kubebuilder:scaffold:imports`) - `Filesystem` - abstraction over afero for testability ### Scaffolded Project Structure Projects generated by the Kubebuilder CLI use the default plugin bundle (`go/v4` + `kustomize/v2`). Each plugin scaffolds different files: **`go/v4` plugin scaffolds Go code:** - `cmd/main.go` - Entry point (manager setup) - `api/v1/*_types.go` - API definitions with `+kubebuilder` markers (via `create api`) - `internal/controller/*_controller.go` - Reconcile logic (via `create api`) - `Dockerfile`, `Makefile` - Build and deployment automation **`kustomize/v2` plugin scaffolds manifests:** - `config/` - Kustomize base manifests (CRDs, RBAC, manager, webhooks) - `config/crd/` - Custom Resource Definitions (via `create api`) - `config/samples/` - Example CR manifests (via `create api`) **`PROJECT` file:** - Project configuration tracking plugins, resources, domain, and layout **Note:** These are files in projects generated BY Kubebuilder, not the Kubebuilder source code itself. ### Reconciliation Pattern Controllers implement `Reconcile(ctx, req) (ctrl.Result, error)`: - **Idempotent** - Safe to run multiple times - **Level-triggered** - React to current state, not events - **Requeue on pending work** - Return `ctrl.Result{Requeue: true}` ### Testing Pattern E2E tests use `utils.TestContext` from `test/e2e/utils/test_context.go`: ```go ctx := utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on") ctx.Init("--domain", "example.com", "--repo", "example.com/project") ctx.CreateAPI("--group", "crew", "--version", "v1", "--kind", "Captain") ctx.Make("build", "test") ctx.LoadImageToKindCluster() ``` ## CLI Reference After `make install`: ```bash kubebuilder init --domain example.com --repo github.com/example/myproject kubebuilder create api --group batch --version v1 --kind CronJob kubebuilder create webhook --group batch --version v1 --kind CronJob kubebuilder edit --plugins=helm/v2-alpha kubebuilder alpha generate # Experimental: generate from PROJECT file kubebuilder alpha update # Experimental: update to latest plugin versions ``` ## Common Patterns ### Code Style - Avoid abbreviations: `context` not `ctx` (except receivers) - Descriptive names: `projectConfig` not `pc` - Single/double-letter receivers OK: `(c CLI)` or `(p Plugin)` ### Logging Conventions Kubebuilder has two distinct types of code with different logging conventions: **1. Kubebuilder CLI Tool Code** → Go CLI best practices Applies to: `pkg/cli/*`, `pkg/plugins/*`, `pkg/machinery/*`, `pkg/config/*`, `pkg/model/*`, etc. This is the Kubebuilder tool itself. Follow Go logging conventions for CLI tools: - **First word lowercase**, sentences after periods capitalized: `"unable to find file. This file is required for..."` - **No ending punctuation** (but use periods between sentences) - **Error strings lowercase**: `fmt.Errorf("something bad")` ```go log.Info("writing scaffold for you to edit") log.Warn("unable to find boilerplate file. This file is used to generate the license header") log.Error("failed to read file", "file", path) return fmt.Errorf("failed to load config: %w", err) ``` **2. Generated Code (Template Output)** → Kubernetes conventions Applies to: Code GENERATED by templates in `pkg/plugins/*/scaffolds/internal/templates/*` Templates produce controller code that runs in Kubernetes clusters. The GENERATED code follows Kubernetes conventions: - **Start with capital letter**: `"Starting reconciliation"` - **No ending period** (but use periods between sentences) - **Past tense**: `"Failed to create Pod"` not `"Cannot create Pod"` - **Active voice**: specify subject or omit when it's the program itself - **Specify object type**: `"Created Deployment"` not `"Created"` ```go // In template files that generate controller code: log.Info("Starting reconciliation") log.Info("Created Deployment", "name", deploy.Name) log.Error(err, "Failed to create Pod", "name", name) ``` **Note:** The distinction is based on WHERE the code runs: - CLI tool (runs on developer's machine) → Go conventions - Generated controllers (run in Kubernetes cluster) → Kubernetes conventions ### Testing Philosophy - Test behaviors, not implementations - Use real components over mocks - Test cases as specifications (Ginkgo: `Describe`, `It`, `Context`, `By`) - Use **Ginkgo v2** + **Gomega** for BDD-style tests. - Tests depending on the Kubebuilder binary should use: `utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on")` ### Test Organization - **Unit tests** (`*_test.go` in `pkg/`) - Test individual packages in isolation, fast - **Integration tests** (`*_integration_test.go` in `pkg/`) - Test multiple components together without cluster - Must have `//go:build integration` tag at the top - May create temp dirs, download binaries, or scaffold files - Examples: alpha update, grafana scaffolding, helm chart generation - **E2E tests** (`test/e2e/`) - **ONLY** for tests requiring a Kubernetes cluster (KIND) - `v4/plugin_cluster_test.go` - Test v4 plugin deployment - `helm/plugin_cluster_test.go` - Test Helm chart deployment - `deployimage/plugin_cluster_test.go` - Test deploy-image plugin ### Scaffolding - Use library helpers from `pkg/plugin/util/` - Use markers for extensibility - Follow existing template patterns in `pkg/machinery` ## Search Tips ```bash rg "\\+kubebuilder:scaffold" --type go # Find markers rg "type.*Plugin struct" pkg/plugins/ # Plugin implementations rg "PluginBundle" pkg/cli/ # Plugin registration rg "func.*SetTemplateDefaults" # Template definitions rg "func new.*Command" pkg/cli/ # CLI commands rg "NewTestContext" test/e2e/ # E2E test setup ``` ## Design Philosophy - **Libraries over code generation** - Use libraries when possible; generated code is hard to maintain - **Common cases easy, uncommon cases possible** - 80-90% use cases should be simple - **Batteries included** - Projects should be deployable/testable out-of-box - **No copy-paste** - Refactor into libraries or remote Kustomize bases ## References ### Essential Files - **`Makefile`** - All automation targets (source of truth for build/test commands) - **`CONTRIBUTING.md`** - CLA, pre-submit checklist, PR requirements - **`VERSIONING.md`** - Release workflow, versioning policy, PR tagging - **`go.mod`** - Go version and dependencies ### Key Directories - **`pkg/`** - Core Kubebuilder code (CLI, plugins, machinery) - **`test/e2e/`** - End-to-end tests with Kubernetes cluster - **`testdata/`** - Generated sample projects (regenerated automatically) - **`docs/book/`** - User documentation source (https://book.kubebuilder.io) ### Important Code Files - **`pkg/cli/init.go`** - Default plugin bundle definition - **`pkg/plugin/interface.go`** - Plugin interface definitions - **`pkg/machinery/scaffold.go`** - Scaffolding engine - **`test/e2e/utils/test_context.go`** - E2E test helpers - **`cmd/version.go`** - Version info (includes K8S version) ### Scripts - **`test/testdata/generate.sh`** - Regenerate all testdata projects - **`hack/docs/generate.sh`** - Regenerate documentation samples - **`test/e2e/local.sh`** - Run e2e tests locally with Kind ### External Resources - **Kubebuilder Book**: https://book.kubebuilder.io - **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder - **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime - **controller-tools**: https://github.com/kubernetes-sigs/controller-tools - **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md - **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ - **Kubernetes Logging Conventions:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines - **Structured Logging Guidelines:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/migration-to-structured-logging.md ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing guidelines This document describes how to contribute to the project. ## Sign the CLA Kubernetes projects require that you sign a Contributor License Agreement (CLA) before we can accept your pull requests. Please see https://git.k8s.io/community/CLA.md for more info. ## Prerequisites - [go](https://golang.org/dl/) version v1.23+. - [docker](https://docs.docker.com/install/) version 17.03+. - [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) version v1.11.3+. - [kustomize](https://github.com/kubernetes-sigs/kustomize/blob/master/site/content/en/docs/Getting%20started/installation.md) v3.1.0+ - Access to a Kubernetes v1.11.3+ cluster. ## Contributing steps 1. Submit an issue describing your proposed change to the repo in question. 1. The [repo owners](OWNERS) will respond to your issue promptly. 1. If your proposed change is accepted, and you haven't already done so, sign a Contributor License Agreement (see details above). 1. Fork the desired repo, develop and test your code changes. 1. Submit a pull request. In addition to the above steps, we adhere to the following best practices to maintain consistency and efficiency in our project: - **Single Commit per PR:** Each Pull Request (PR) should contain only one commit. This approach simplifies tracking changes and makes the history more readable. - **One Issue per PR:** Each PR should address a single specific issue or need. This helps in streamlining our workflow and makes it easier to identify and resolve problems such as revert the changes if required. For more detailed guidelines, refer to the [Kubernetes Contributor Guide][k8s-contrubutiong-guide]. ## How to build kubebuilder locally Note that, by building the kubebuilder from the source code we are allowed to test the changes made locally. 1. Run the following command to clone your fork of the project locally in the dir /src/sigs.k8s.io/kubebuilder ``` $ git clone git@github.com:/kubebuilder.git $GOPATH/src/sigs.k8s.io/kubebuilder ``` 1. Ensure you activate module support before continue (`$ export GO111MODULE=on`) 1. Run the command `make install` to create a bin with the source code **NOTE** In order to check the local environment run `make test-unit`. ## What to do before submitting a pull request 1. Run the script `make generate` to update/generate the mock data used in the e2e test in `$GOPATH/src/sigs.k8s.io/kubebuilder/testdata/` 1. Run `make test-unit test-e2e-local` - e2e tests use [`kind`][kind] and [`setup-envtest`][setup-envtest]. If you want to bring your own binaries, place them in `$(go env GOPATH)/bin`. **IMPORTANT:** The `make generate` is very helpful. By using it, you can check if good part of the commands still working successfully after the changes. Also, note that its usage is a prerequisite to submit a PR. Following the targets that can be used to test your changes locally. | Command | Description | Is called in the CI? | | ------------------- | ------------------------------------------------------------- | -------------------- | | make test-unit | Runs go tests | no | | make test | Runs tests in shell (`./test.sh`) | yes | | make lint | Run [golangci][golangci] lint checks | yes | | make lint-fix | Run [golangci][golangci] to automatically perform fixes | no | | make test-coverage | Run coveralls to check the % of code covered by tests | yes | | make check-testdata | Checks if the testdata dir is updated with the latest changes | yes | | make test-e2e-local | Runs the CI e2e tests locally | no | **NOTE** `make lint` requires a local installation of `golangci-lint`. More info: https://github.com/golangci/golangci-lint#install ### Running e2e tests locally See that you can run `test-e2e-local` to setup Kind and run e2e tests locally. Another option is by manually starting up Kind and configuring it and then, you can for example via your IDEA debug the e2e tests. To manually setup run: ```shell # To generate an Kubebuilder local binary with your changes make install # To create the cluster kind create cluster --config ./test/e2e/kind-config.yaml ``` Now, you can for example, run in debug mode the `test/e2e/all/e2e_suite_test.go`: ![example](https://github.com/kubernetes-sigs/kubebuilder/assets/7708031/277d26d5-c94d-41f0-8f02-1381458ef750) ### Test Plugin If your intended PR creates a new plugin, make sure the PR also provides test cases. Testing should include: 1. `e2e tests` to validate the behavior of the proposed plugin. 2. `sample projects` to verify the scaffolded output from the plugin. #### 1. Plugin E2E Tests All the plugins provided by Kubebuilder should be validated through `e2e-tests` across multiple platforms. Current Kubebuilder provides the testing framework that includes testing code based on [ginkgo](https://github.com/onsi/ginkgo), [Github Actions](https://github.com/Kavinjsir/kubebuilder/blob/docs%2Ftest-plugin/.github/workflows/testdata.yml) for unit tests, and multiple env tests driven by [test-infra](https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes-sigs/kubebuilder/kubebuilder-presubmits.yaml). To fully test the proposed plugin: 1. Add test specs to `test/e2e/plugin__test.go` in the unified test suite. 2. Tests should use the shared `e2e_suite_test.go` BeforeSuite/AfterSuite hooks (cert-manager and Prometheus are already installed). 3. Each test should: - Initialize a `TestContext` using `utils.NewTestContext` - Trigger the plugin's bound subcommands. See [Init](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/utils/test_context.go#L213), [CreateAPI](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.6.0/test/e2e/utils/test_context.go#L222) - Use [PluginUtil](https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin/util) to verify the scaffolded outputs 4. Test validation should: - 4.1. Setup testing environment, e.g: - Cleanup environment, create temp dir. See [Prepare](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/utils/test_context.go#L97) - If your test will cover the provided features then, ensure that you install prerequisites CRDs: See [InstallCertManager](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/utils/test_context.go#L138), [InstallPrometheusManager](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.6.0/test/e2e/utils/test_context.go#L171) - 4.2. Run the function from `generate_test.go`. - 4.3. Further make sure the scaffolded output works, e.g: - Execute commands in your `Makefile`. See [Make](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/utils/test_context.go#L240) - Temporary load image of the testing controller. See [LoadImageToKindCluster](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/utils/test_context.go#L283) - Call Kubectl to validate running resources. See [utils.Kubectl](https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#Kubectl) - 4.4. Delete temporary resources after testing exited, e.g: - Uninstall prerequisites CRDs: See [UninstallPrometheusOperManager](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/utils/test_context.go#L183) - Delete temp dir. See [Destroy](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/utils/test_context.go#L255) 5. Add the command in [test/e2e/plugin](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/e2e/setup.sh#L65) to run your testing code: ```shell go test $(dirname "$0")/ $flags -timeout 30m ``` #### 2. Sample Projects from the Plugin It is also necessary to test consistency of the proposed plugin across different env and the integration with other plugins. This is performed by generating sample projects based on the plugins. The CI workflow defined in Github Action would validate the availability and the consistency. See: - [test/testdata/generated.sh](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/test/testdata/generate.sh#L144) - [make generate](https://github.com/kubernetes-sigs/kubebuilder/blob/v3.7.0/Makefile#L70) ## PR Process See [VERSIONING.md](VERSIONING.md) for a full description. TL;DR: ### PR Title Format PR titles use **emojis** (appear in release notes). Format: `:emoji: (plugin/version): Description` **Emojis:** - ⚠️ (`:warning:`) - Breaking change - ✨ (`:sparkles:`) - New feature - 🐛 (`:bug:`) - Bug fix - 📖 (`:book:`) - Documentation - 🌱 (`:seedling:`) - Infrastructure/tests/refactor - 👻 (`:ghost:`) - No release note (unreleased changes only) **Examples:** ``` 🐛 Resolve nil pointer panic in scaffold generator ✨ (helm/v2-alpha): Add cluster-scoped resource support 📖 (go/v4): Update deployment documentation ✨ Update dependencies to latest versions 🌱 Add new GitHub action to test out doc samples ``` ### Commit Message Format Commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/) standard. Format: `[optional scope]: ` The `[optional scope]` is typically the plugin/version (e.g., `helm/v2-alpha`, `go/v4`); omit it for repo-wide or non-plugin changes. **Types:** - **feat**: A new feature for the user or a plugin - **fix**: A bug fix for the user or a plugin - **docs**: Documentation changes only - **test**: Adding or updating tests - **refactor**: Code change that neither fixes a bug nor adds a feature - **chore**: Changes to build process, dependencies, or maintenance tasks - **breaking**: A breaking change (can be combined with other types) **Examples:** ``` fix: Resolve nil pointer panic in scaffold generator feat(helm/v2-alpha): Add cluster-scoped resource support docs(go/v4): Update deployment documentation chore: Update dependencies to latest versions ``` ## Where the CI Tests are configured 1. See the [action files](.github/workflows) to check its tests, and the scripts used on it. 2. Note that the prow tests used in the CI are configured in [kubernetes-sigs/kubebuilder/kubebuilder-presubmits.yaml](https://github.com/kubernetes/test-infra/blob/master/config/jobs/kubernetes-sigs/kubebuilder/kubebuilder-presubmits.yaml). 3. Check that all scripts used by the CI are defined in the project. 4. Notice that our policy to test the project is to run against k8s version N-2. So that the old version should be removed when there is a new k8s version available. ## How to contribute to docs The docs are published off of three branches: - `book-v4`: [book.kubebuilder.io](https://book.kubebuilder.io) -- current docs - `book-v3`: [book-v3.book.kubebuilder.io](https://book-v3.book.kubebuilder.io) -- legacy docs - `book-v2`: [book-v2.book.kubebuilder.io](https://book-v2.book.kubebuilder.io) -- legacy docs - `book-v1`: [book-v1.book.kubebuilder.io](https://book-v1.book.kubebuilder.io) -- legacy docs - `master`: [master.book.kubebuilder.io](https://master.book.kubebuilder.io) -- "nightly" docs See [VERSIONING.md](VERSIONING.md#book-releases) for more information. The documentation is rendered using [mdBook with its advanced Markdown features](https://rust-lang.github.io/mdBook/format/markdown.html). There are certain writing style guidelines for Kubernetes documentation, checkout [style guide](https://kubernetes.io/docs/contribute/style/style-guide/) for more information. ### How to preview the changes performed in the docs Check the CI job after to do the Pull Request and then, click on in the `Details` of `netlify/kubebuilder/deploy-preview` ## Community, discussion and support Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). You can reach the maintainers of this project at: - [Slack](http://slack.k8s.io/) - [Mailing List](https://groups.google.com/forum/#!forum/kubebuilder) ## Becoming a reviewer or approver Contributors may eventually become official reviewers or approvers in Kubebuilder and the related repositories. See [CONTRIBUTING-ROLES.md](docs/CONTRIBUTING-ROLES.md) for more information. ## Code of conduct Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). [golangci]: https://github.com/golangci/golangci-lint [kind]: https://kind.sigs.k8s.io/#installation-and-usage [setup-envtest]: https://book.kubebuilder.io/reference/envtest [k8s-contrubutiong-guide]: https://www.kubernetes.dev/docs/guide/contributing/ ================================================ FILE: DESIGN.md ================================================ # Kubebuilder Design Principles This lays out some of the guiding design principles behind the Kubebuilder project and its various components. ## Overarching * **Libraries Over Code Generation**: Generated code is messy to maintain, hard for humans to change and understand, and hard to update. Library code is easy to update (just increase your dependency version), easier to version using existing mechanisms, and more concise. * **Copy-pasting is bad**: Copy-pasted code suffers from similar problems as code generation, except more acutely. Copy-pasted code is nearly impossible to easy update, and frequently suffers from bugs and misunderstandings. If something is being copy-pasted, it should refactored into a library component or remote [kustomize](https://sigs.k8s.io/kustomize) base. * **Common Cases Should Be Easy**: The 80-90% common cases should be simple and easy for users to understand. * **Uncommon Cases Should Be Possible**: There shouldn't be situations where it's downright impossible to do something within controller-runtime or controller-tools. It may take extra digging or coding, and it may involve interoperating with lower-level components, but it should be possible without unreasonable friction. ## Kubebuilder * **Kubebuilder Has Opinions**: Kubebuilder exists as an opinionated project generator. It should strive to give users a reasonable project layout that's simple enough to understand when getting started, but provides room to grow. It might not match everyone's opinions, but it should strive to be useful to most. * **Batteries Included**: Kubebuilder projects should contain enough deployment information to reasonably develop and run the scaffolded project. This includes testing, deployment files, and development infrastructure to go from code to running containers. ## controller-tools and controller-runtime * **Sufficient But Composable**: controller-tools and controller-runtime should be sufficient for building a custom controller by hand. While scaffolding and additional libraries may make life easier, building without should be as painless as possible. That being said, they should strive to be usable as building blocks for higher-level libraries as well. * **Self-Sufficient Docs**: controller-tools and controller-runtime should strive to have self-sufficient docs (i.e. documentation that doesn't require reading other libraries' documentation for common use cases). Examples should be plentiful. * **Contained Arcana**: Developers should not need to be experts in Kubernetes API machinery to develop controllers, but those familiar with Kubernetes API machinery should not feel out of place. Abstractions should be intuitive to new users but feel familiar to experienced ones. Abstractions should embrace the concepts of Kubernetes (e.g. declarative idempotent reconcilers) while simplifying the details. ## controller-runtime * **Abstractions Should Be Layered**: Abstractions should be built on top of lower layers, such that advanced users can write custom logic while still working within the existing model. For instance, the controller builder is built on top of the event, source, and handler helpers, which are in turn built for use with the event, source, and handler interfaces. * **Repetitive Stress Injuries Are Bad**: When possible, commonly used pieces should be exposed in a way that enables clear, concise code. This includes aliasing groups of functionality under "alias" or "prelude" packages to avoid having 40 lines of imports, including common idioms as flexible helpers, and infering resource information from the user's object types in client code. * **A Little Bit of Magic Goes a Long Way**: In absence of generics, reflection is acceptable, especially when it leads to clearer, conciser code. However, when possible interfaces that use reflection should be designed to avoid requiring the end-developer to use type assertions, string splitting, which are error-prone and repetitive. These should be dealt with inside controller-runtime internals. * **Defaults Over Constructors**: When not a huge performance impact, favor auto-defaulting and `Options` structs over constructors. Constructors quickly become unclear due to lack of names associated with values, and don't work well with optional values. ## Development * **Words Are Better Than Letters**: Don't abbreviate variable names unless it's blindingly obvious what they are (e.g. `ctx` for `Context`). Single- and double-letter method receivers are acceptable, but single- and double-letter variables quickly become confusing the longer a code block gets. * **Well-commented code**: Code should be commented and given Godocs, even private methods and functions. It may *seem* obvious what they do at the time and why, but you might forget, and others will certainly come along. * **Test Behaviors**: Test cases should be comprehensible as sets of expected behaviors. Test cases read without code (e.g. just using `It`, `Describe`, `Context`, and `By` lines) should still be able to explain what's required of the tested interface. Testing behaviors makes internal refactors easier, and makes reading tests easier. * **Real Components Over Mocks**: Avoid mocks and recording actions. Mocks tend to be brittle and gradually become more complicated over time (e.g. fake client implementations tend to grow into poorly-written, incomplete API servers). Recording of actions tends to lead to brittle tests that requires changes during refactors. Instead, test that the end desired state is correct. Test the way the world should be, without caring how it got there, and provide easy ways to set up the real components so that mocks aren't required. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ #!/usr/bin/env bash # Copyright 2023 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. # # Makefile with some common workflow for dev, build and test # export GOPROXY?=https://proxy.golang.org/ # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p $(LOCALBIN) ##@ General # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the # target descriptions by '##'. The awk command is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. # More info on the usage of ANSI control characters for terminal formatting: # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters # More info on the awk command: # http://linuxcommand.org/lc3_adv_awk.php .PHONY: help help: ## Display this help @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ##@ Build .PHONY: build build: ## Build the project locally go build --trimpath -o bin/kubebuilder .PHONY: install install: build ## Build and install the binary with the current source code. Use it to test your changes locally. rm -f $(GOBIN)/kubebuilder cp ./bin/kubebuilder $(GOBIN)/kubebuilder ##@ Development .PHONY: generate generate: generate-testdata generate-docs ## Update/generate all mock data. You should run this commands to update the mock data after your changes. go mod tidy make remove-spaces .PHONY: remove-spaces remove-spaces: @echo "Removing trailing spaces" @bash -c ' \ if sed --version 2>&1 | grep -q "GNU"; then \ find . -type f -name "*.md" -exec sed -i "s/[[:space:]]*$$//" {} + || true; \ else \ find . -type f -name "*.md" -exec sed -i "" "s/[[:space:]]*$$//" {} + || true; \ fi' .PHONY: generate-testdata generate-testdata: ## Update/generate the testdata in $GOPATH/src/sigs.k8s.io/kubebuilder chmod -R +w testdata/ rm -rf testdata/ ./test/testdata/generate.sh .PHONY: generate-docs generate-docs: ## Update/generate the docs ./hack/docs/generate.sh .PHONY: generate-charts generate-charts: build ## Re-generate the helm chart testdata and docs samples rm -rf testdata/project-v4-with-plugins/dist/chart rm -rf docs/book/src/getting-started/testdata/project/dist/chart rm -rf docs/book/src/cronjob-tutorial/testdata/project/dist/chart rm -rf docs/book/src/multiversion-tutorial/testdata/project/dist/chart # Generate helm charts from kustomize manifests using v2-alpha plugin (cd testdata/project-v4-with-plugins && make build-installer && ../../bin/kubebuilder edit --plugins=helm/v2-alpha) (cd docs/book/src/getting-started/testdata/project && make build-installer && ../../../../../../bin/kubebuilder edit --plugins=helm/v2-alpha) (cd docs/book/src/cronjob-tutorial/testdata/project && make build-installer && ../../../../../../bin/kubebuilder edit --plugins=helm/v2-alpha) (cd docs/book/src/multiversion-tutorial/testdata/project && make build-installer && ../../../../../../bin/kubebuilder edit --plugins=helm/v2-alpha) .PHONY: check-docs check-docs: ## Run the script to ensure that the docs are updated ./hack/docs/check.sh .PHONY: lint lint: golangci-lint yamllint check-sample-permissions ## Run golangci-lint linter, yamllint & sample permissions check $(GOLANGCI_LINT) run .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes $(GOLANGCI_LINT) run --fix .PHONY: lint-config lint-config: golangci-lint ## Verify golangci-lint linter configuration $(GOLANGCI_LINT) config verify # Lint all YAML: testdata files (yamllint-yaml) + Helm-rendered charts (yamllint-helm). # Repo YAML uses .yamllint; Helm output uses .yamllint-helm. YAMLLINT_FILES := $(shell find testdata -name '*.yaml' ! -path 'testdata/.helm-rendered.yaml' \( ! -path 'testdata/*/dist/*' -o -path 'testdata/*/dist/chart/Chart.yaml' -o -path 'testdata/*/dist/chart/values.yaml' \) 2>/dev/null) HELM_CHARTS := $(shell find testdata docs/book -type d -path '*/dist/chart' 2>/dev/null) .PHONY: yamllint yamllint-yaml yamllint-helm yamllint: yamllint-yaml yamllint-helm yamllint-yaml: @docker run --rm $$(tty -s && echo "-it" || echo) -v $(PWD):/data -w /data cytopia/yamllint:latest $(YAMLLINT_FILES) -c .yamllint --no-warnings yamllint-helm: @for chart in $(HELM_CHARTS); do \ helm template release $$chart --namespace=release-system 2>/dev/null | \ docker run --rm -i -v $(PWD):/data -w /data cytopia/yamllint:latest -c .yamllint-helm --no-warnings - || (echo "yamllint-helm: $$chart failed"; exit 1); \ done # All Kubebuilder-generated samples (go/v4, kustomize, helm use machinery defaults 0755/0644). SAMPLE_ROOTS := testdata \ docs/book/src/getting-started/testdata \ docs/book/src/cronjob-tutorial/testdata \ docs/book/src/multiversion-tutorial/testdata .PHONY: check-sample-permissions check-sample-permissions: ## Fail if any file/dir under testdata or docs samples has wrong permissions (expect 0644/0755). bin/ excluded. @for d in $(SAMPLE_ROOTS); do \ test -d "$$d" || continue; \ bad=$$(find "$$d" -path '*/bin' -prune -o \( \( -type f ! -perm 0644 \) -o \( -type d ! -perm 0755 \) \) -print 2>/dev/null); \ if [ -n "$$bad" ]; then echo "Invalid permissions under $$d (expect 0644/0755):"; echo "$$bad"; exit 1; fi; \ done .PHONY: golangci-lint golangci-lint: $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,${GOLANGCI_LINT_VERSION}) .PHONY: apidiff apidiff: go-apidiff ## Run the go-apidiff to verify any API differences compared with origin/master $(GO_APIDIFF) master --compare-imports --print-compatible --repo-path=. .PHONY: go-apidiff go-apidiff: $(call go-install-tool,$(GO_APIDIFF),github.com/joelanford/go-apidiff,$(GO_APIDIFF_VERSION)) ##@ Tests .PHONY: test test: test-unit test-integration test-testdata test-book test-license test-gomod ## Run the unit and integration tests (used in the CI) .PHONY: test-unit TEST_PKGS := ./pkg/... ./test/e2e/utils/... test-unit: ## Run the unit tests go test -race $(TEST_PKGS) .PHONY: test-integration test-integration: install ## Run the integration tests (requires kubebuilder binary in PATH) go test -race -tags=integration -timeout 30m $(TEST_PKGS) .PHONY: test-coverage test-coverage: ## Run unit and integration tests with coverage report - rm -rf *.out # Remove all coverage files if exists go test -race -failfast -tags=integration -timeout 30m \ -coverprofile=coverage-all.out \ -coverpkg="\ ./pkg/cli/...,\ ./pkg/config/...,\ ./pkg/internal/...,\ ./pkg/machinery/...,\ ./pkg/model/...,\ ./pkg/plugin/...,\ ./pkg/plugins/golang,\ ./pkg/plugins/golang/deploy-image/v1alpha1,\ ./pkg/plugins/golang/v4,\ ./pkg/plugins/external/...,\ ./pkg/plugins/common/kustomize/v2,\ ./pkg/plugins/optional/autoupdate/v1alpha,\ ./pkg/plugins/optional/grafana/...,\ ./pkg/plugins/optional/helm/v2alpha/..." \ $(TEST_PKGS) .PHONY: check-testdata check-testdata: ## Run the script to ensure that the testdata is updated ./test/testdata/check.sh .PHONY: test-testdata test-testdata: ## Run the tests of the testdata directory ./test/testdata/test.sh .PHONY: test-e2e-local test-e2e-local: ## Run the end-to-end tests locally ## To keep the same kind cluster between test runs, use `SKIP_KIND_CLEANUP=1 make test-e2e-local` ./test/e2e/local.sh .PHONY: test-e2e-ci test-e2e-ci: ## Run the end-to-end tests (used in the CI)` ./test/e2e/ci.sh .PHONY: test-book test-book: ## Run the cronjob tutorial's unit tests to make sure we don't break it cd ./docs/book/src/cronjob-tutorial/testdata/project && make test cd ./docs/book/src/multiversion-tutorial/testdata/project && make test cd ./docs/book/src/getting-started/testdata/project && make test .PHONY: test-license test-license: ## Run the license check ./test/check-license.sh .PHONY: test-gomod test-gomod: ## Run the Go module compatibility check go run ./hack/test/check_go_module.go .PHONY: test-external-plugin test-external-plugin: install ## Run tests for external plugin make -C docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1 install make -C docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1 test-plugin .PHONY: test-spaces test-spaces: ## Run the trailing spaces check ./test/check_spaces.sh ## TODO: Remove me when go/v4 plugin be removed ## Deprecated .PHONY: test-legacy test-legacy: ## Run the tests to validate legacy path for webhooks rm -rf ./testdata/**legacy**/ ./test/testdata/legacy-webhook-path.sh .PHONY: install-helm install-helm: ## Install the latest version of Helm locally @curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash .PHONY: helm-lint helm-lint: install-helm ## Lint the Helm chart in testdata helm lint testdata/project-v4-with-plugins/dist/chart ## Tool Binaries GO_APIDIFF ?= $(LOCALBIN)/go-apidiff GOLANGCI_LINT ?= $(LOCALBIN)/golangci-lint ## Tool Versions GO_APIDIFF_VERSION ?= v0.8.3 GOLANGCI_LINT_VERSION ?= v2.8.0 # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed # $3 - specific version of package define go-install-tool @[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ set -e; \ package=$(2)@$(3) ;\ echo "Downloading $${package}" ;\ rm -f $(1) ;\ GOBIN=$(LOCALBIN) go install $${package} ;\ mv $(1) $(1)-$(3) ;\ } ;\ ln -sf $$(realpath $(1)-$(3)) $(1) endef ================================================ FILE: OWNERS ================================================ # See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md approvers: - kubebuilder-admins - kubebuilder-approvers reviewers: - kubebuilder-admins - kubebuilder-reviewers - kubebuilder-approvers ================================================ FILE: OWNERS_ALIASES ================================================ # See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md aliases: # active folks who can be contacted to perform admin-related # tasks on the repo, or otherwise approve any PRs. kubebuilder-admins: - camilamacedo86 - varshaprasad96 # non-admin folks who can approve any PRs in the repo # kubebuilder-approvers: # folks who can review and LGTM any PRs in the repo (doesn't include # approvers & admins -- those count too via the OWNERS file) kubebuilder-reviewers: - vitorfloriano # folks who may have context on ancient history, # but are no longer directly involved kubebuilder-emeritus-approvers: - adirio - directxman12 - droot - estroz - jmrodri - joelanford - Kavinjsir - mengqiy - pwittrock kubebuilder-emeritus-reviewers: - everettraven - rashmigottipati ================================================ FILE: README.md ================================================ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/kubernetes-sigs/kubebuilder/badge)](https://scorecard.dev/viewer/?uri=github.com/kubernetes-sigs/kubebuilder) [![Lint](https://github.com/kubernetes-sigs/kubebuilder/actions/workflows/lint.yml/badge.svg)](https://github.com/kubernetes-sigs/kubebuilder/actions/workflows/lint.yml) [![Go Report Card](https://goreportcard.com/badge/sigs.k8s.io/kubebuilder)](https://goreportcard.com/report/sigs.k8s.io/kubebuilder) [![Coverage Status](https://coveralls.io/repos/github/kubernetes-sigs/kubebuilder/badge.svg?branch=master)](https://coveralls.io/github/kubernetes-sigs/kubebuilder?branch=master) [![Latest release](https://img.shields.io/github/v/release/kubernetes-sigs/kubebuilder)](https://github.com/kubernetes-sigs/kubebuilder/releases) [![Go Reference](https://pkg.go.dev/badge/sigs.k8s.io/kubebuilder/v4.svg)](https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4) ## Kubebuilder Kubebuilder is a framework for building Kubernetes APIs using [custom resource definitions (CRDs)](https://kubernetes.io/docs/tasks/access-kubernetes-api/extend-api-custom-resource-definitions). Similar to web development frameworks such as *Ruby on Rails* and *SpringBoot*, Kubebuilder increases velocity and reduces the complexity managed by developers for rapidly building and publishing Kubernetes APIs in Go. It builds on top of the canonical techniques used to build the core Kubernetes APIs to provide simple abstractions that reduce boilerplate and toil. Kubebuilder does **not** exist as an example to *copy-paste*, but instead provides powerful libraries and tools to simplify building and publishing Kubernetes APIs from scratch. It provides a plugin architecture allowing users to take advantage of optional helpers and features. To learn more about this see the [Plugin section][plugin-section]. Kubebuilder is developed on top of the [controller-runtime][controller-runtime] and [controller-tools][controller-tools] libraries. ### Kubebuilder is also a library Kubebuilder is extensible and can be used as a library in other projects. [Operator-SDK][operator-sdk] is a good example of a project that uses Kubebuilder as a library. [Operator-SDK][operator-sdk] uses the plugin feature to include non-Go operators _e.g. operator-sdk's Ansible and Helm-based language Operators_. To learn more see [how to create your own plugins][your-own-plugins]. ### Installation It is strongly recommended that you use a released version. Release binaries are available on the [releases](https://github.com/kubernetes-sigs/kubebuilder/releases) page. Follow the [instructions](https://book.kubebuilder.io/quick-start.html#installation) to install Kubebuilder. ## Getting Started See the [Getting Started](https://book.kubebuilder.io/quick-start.html) documentation. Also, ensure that you check out the [Deploy Image](./docs/book/src/plugins/available/deploy-image-plugin-v1-alpha.md) Plugin. This plugin allows users to scaffold API/Controllers to deploy and manage an Operand (image) on the cluster following the guidelines and best practices. It abstracts the complexities of achieving this goal while allowing users to customize the generated code. ## Documentation Check out the Kubebuilder [book](https://book.kubebuilder.io). ## Resources - Kubebuilder Book: [book.kubebuilder.io](https://book.kubebuilder.io) - GitHub Repo: [kubernetes-sigs/kubebuilder](https://github.com/kubernetes-sigs/kubebuilder) - Slack channel: [#kubebuilder](https://kubernetes.slack.com/messages/#kubebuilder) - Google Group: [kubebuilder@googlegroups.com](https://groups.google.com/forum/#!forum/kubebuilder) - Design Documents: [designs](designs/) - Plugin: [plugins][plugin-section] ## Motivation Building Kubernetes tools and APIs involves making a lot of decisions and writing a lot of boilerplate. To facilitate easily building Kubernetes APIs and tools using the canonical approach, this framework provides a collection of Kubernetes development tools to minimize toil. Kubebuilder attempts to facilitate the following developer workflow for building APIs 1. Create a new project directory 2. Create one or more resource APIs as CRDs and then add fields to the resources 3. Implement reconcile loops in controllers and watch additional resources 4. Test by running against a cluster (self-installs CRDs and starts controllers automatically) 5. Update bootstrapped integration tests to test new fields and business logic 6. Build and publish a container from the provided Dockerfile ## Scope Building APIs using CRDs, Controllers, and Admission Webhooks. ## Philosophy See [DESIGN.md](DESIGN.md) for the guiding principles of the various Kubebuilder projects. TL;DR: Provide clean library abstractions with clear and well-exampled go docs. - Prefer using go *interfaces* and *libraries* over-relying on *code generation* - Prefer using *code generation* over *1 time init* of stubs - Prefer *1 time init* of stubs over forked and modified boilerplate - Never fork and modify boilerplate ## Techniques - Provide higher-level libraries on top of low-level client libraries - Protect developers from breaking changes in low-level libraries - Start minimal and provide progressive discovery of functionality - Provide sane defaults and allow users to override when they exist - Provide code generators to maintain common boilerplate that can't be addressed by interfaces - Driven off of `// +` comments - Provide bootstrapping commands to initialize new packages ## Versioning and Releasing See [VERSIONING.md](VERSIONING.md). ## Troubleshooting - ### Bugs and Feature Requests: If you have what looks like a bug, or you would like to make a feature request, please use the [Github issue tracking system.](https://github.com/kubernetes-sigs/kubebuilder/issues) Before you file an issue, please search existing issues to see if your issue is already covered. - ### Slack For real-time discussion, you can join the [#kubebuilder](https://slack.k8s.io/#kubebuilder) slack channel. Slack requires registration, but the Kubernetes team is an open invitation to anyone to register here. Feel free to come and ask any questions. ## Contributing Contributions are greatly appreciated. The maintainers actively manage the issues list and try to highlight issues suitable for newcomers. The project follows the typical GitHub pull request model. See [CONTRIBUTING.md](CONTRIBUTING.md) for more details. Before starting any work, please either comment on an existing issue or file a new one. ## Operating Systems Supported Currently, Kubebuilder officially supports macOS and Linux platforms. If you are using a Windows OS, we recommend you read the instructions in [here](docs/windows.md). Contributions towards supporting Windows are not planned. ## Versions Compatibility and Supportability Projects created by Kubebuilder contain a `Makefile` that installs tools at versions defined during project creation. The main tools included are: - [kustomize](https://github.com/kubernetes-sigs/kustomize) - [controller-gen](https://github.com/kubernetes-sigs/controller-tools) - [setup-envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/main/tools/setup-envtest) Additionally, these projects include a `go.mod` file specifying dependency versions. Kubebuilder relies on [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) and its Go and Kubernetes dependencies. Therefore, the versions defined in the `Makefile` and `go.mod` files are the ones that have been tested, supported, and recommended. Each minor version of Kubebuilder is tested with a specific minor version of the client-go. While a Kubebuilder minor version *may* be compatible with other client-go minor versions, or other tools this compatibility is not guaranteed, supported, or tested. The minimum Go version required by Kubebuilder is determined by the highest minimum Go version required by its dependencies. This is usually aligned with the minimum Go version required by the corresponding `k8s.io/*` dependencies. Compatible `k8s.io/*` versions, client-go versions, and minimum Go versions can be found in the `go.mod` file scaffolded for each project for each [tag release](https://github.com/kubernetes-sigs/kubebuilder/tags). **Example:** For the `4.1.1` release, the minimum Go version compatibility is `1.22`. You can refer to the samples in the testdata directory of the tag released [v4.1.1](https://github.com/kubernetes-sigs/kubebuilder/tree/v4.1.1/testdata), such as the [go.mod](https://github.com/kubernetes-sigs/kubebuilder/blob/v4.1.1/testdata/project-v4/go.mod#L3) file for `project-v4`. You can also check the versions of the tools supported and tested for this release by examining the [Makefile](https://github.com/kubernetes-sigs/kubebuilder/blob/v4.1.1/testdata/project-v4/Makefile#L160-L165). ## Community Meetings The following meetings happen biweekly: - Kubebuilder Meeting You are more than welcome to attend. For further info join to [kubebuilder@googlegroups.com](https://groups.google.com/g/kubebuilder). Every month, our team meets on the first Thursday at 11:00 PT (Pacific Time) to discuss our progress and plan for the upcoming weeks. Please note that we have been syncing more frequently offline via Slack lately. However, if you add a topic to the agenda, we will hold the meeting as scheduled. Additionally, we can use this channel to demonstrate new features. [operator-sdk]: https://github.com/operator-framework/operator-sdk [plugin-section]: https://book.kubebuilder.io/plugins/plugins.html [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [your-own-plugins]: https://book.kubebuilder.io/plugins/extending [controller-tools]: https://github.com/kubernetes-sigs/controller-tools ================================================ FILE: RELEASE.md ================================================ # Release Process The Kubebuilder Project is released on an as-needed basis. The process is as follows: **Note:** Releases are done from the `release-MAJOR.MINOR` branches. For PATCH releases it is not required to create a new branch. Instead, you will just need to ensure that all major fixes are cherry-picked into the respective `release-MAJOR.MINOR` branch. To know more about versioning, check https://semver.org/. **Note:** Before `3.5.*` release this project was released based on `MAJOR`. A change to the process was done to ensure that we have an aligned process under the org (similar to `controller-runtime` and `controller-tools`) and to make it easier to produce patch releases. ## How to do a release ### Create the new branch and the release tag 1. Create a new branch `git checkout -b release-` from master 2. Push the new branch to the remote repository ### Now, let's generate the changelog 1. Create the changelog from the new branch `release-` (`git checkout release-`). You will need to use the [kubebuilder-release-tools][kubebuilder-release-tools] to generate release notes. See [here][release-notes-generation] > **Note** > - You will need to have checkout locally from the remote repository the previous branch > - Also, ensure that you fetch all tags from the remote `git fetch --all --tags` > - Also, if you face issues to generate the release notes you might want to able to sort it out by running i.e.: > `go run sigs.k8s.io/kubebuilder-release-tools/notes --use-upstream=false --from=v3.11.0 --branch=release-X` ### Draft a new release from GitHub 1. Create a new tag with the correct version from the new `release-` branch 2. Verify the Release Github Action. It should build the assets and publish in the draft release 3. You also need to manually add the changelog generated above on the release page and publish it. Now, the source code is released! ### Update the website docs (https://book.kubebuilder.io/quick-start.html) 1. Push a PR to update the `book-v3` branch with the changes of the latest release branch created (`release-`) 2. Ping in the [Kubebuilder Slack channel](https://kubernetes.slack.com/archives/CAR30FCJZ) and ask for reviews. ### When the release be done and the website update: Announce the new release: 1. Announce the new release on the Slack channel, i.e: ```` :announce: Kubebuilder v3.5.0 has been released! This release includes a Kubernetes dependency bump to v1.24. For more info, see the release page: https://github.com/kubernetes-sigs/kubebuilder/releases/tag/v3.5.0 :tada: Thanks to all our contributors! ```` 2. Announce the new release via email is sent to `kubebuilder@googlegroups.com` with the subject `[ANNOUNCE] Kubebuilder $VERSION is released` ## HEAD releases The binaries releases for HEAD are available here: - [kubebuilder-release-master-head-darwin-amd64.tar.gz](https://storage.googleapis.com/kubebuilder-release/kubebuilder-release-master-head-darwin-amd64.tar.gz) - [kubebuilder-release-master-head-linux-amd64.tar.gz](https://storage.googleapis.com/kubebuilder-release/kubebuilder-release-master-head-linux-amd64.tar.gz) ## How the releases are configured The releases occur in an account in the Google Cloud (See [here](https://console.cloud.google.com/cloud-build/builds?project=kubebuilder)) using Cloud Build. ### To build the Kubebuilder CLI binaries: A trigger GitHub action [release](.github/workflows/release.yml) is trigged when a new tag is pushed. This action will call the job [./build/.goreleaser.yml](./build/.goreleaser.yml). ### (Deprecated) - To build the Kubebuilder-tools: (Artifacts required to use ENV TEST) > We no longer build the artifacts and the promotion of those is deprecated. For more info see: https://github.com/kubernetes-sigs/kubebuilder/discussions/4082 Kubebuilder projects requires artifacts which are used to do test with ENV TEST (when we call `make test` target) These artifacts can be checked in the service page: https://storage.googleapis.com/kubebuilder-tools The build is made from the branch [tools-releases](https://github.com/kubernetes-sigs/kubebuilder/tree/tools-releases) and the trigger will call the `build/cloudbuild_tools.yaml` passing as argument the architecture and the OS that should be used, e.g: Screenshot 2022-04-30 at 10 15 41 For further information see the [README](https://github.com/kubernetes-sigs/kubebuilder/blob/tools-releases/README.md). ### (Deprecated) - To build the `kube-rbac-proxy` images: > We no longer build the images and the promotion of those images is deprecated. For more info see: https://github.com/kubernetes-sigs/kubebuilder/discussions/3907 These images are built from the project [brancz/kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy). The projects built with Kubebuilder creates a side container with `kube-rbac-proxy` to protect the Manager. These images can be checked in the console, see [here](https://console.cloud.google.com/gcr/images/kubebuilder/GLOBAL/kube-rbac-proxy). The project `kube-rbac-proxy` is in the process to be donated to the k8s org. However, it is going on for a long time and then, we have no ETA for that to occur. When that occurs we can automate this process. But until there we need to generate these images by bumping the versions/tags released by `kube-rbac-proxy` on the branch [kube-rbac-proxy-releases](https://github.com/kubernetes-sigs/kubebuilder/tree/kube-rbac-proxy-releases) then the `build/cloudbuild_kube-rbac-proxy.yaml` will generate the images. To check an example, see the pull request [#2578](https://github.com/kubernetes-sigs/kubebuilder/pull/2578). **Note**: we cannot use the images produced by the project `kube-rbac-proxy` because we need to ensure to Kubebuilder users that these images will be available. ### (Deprecated) - To build the `gcr.io/kubebuilder/pr-verifier` images: > We are working on to move all out from GCP Kubebuilder project. For further information see: https://github.com/kubernetes/k8s.io/issues/2647#issuecomment-2111182864 These images are used to verify the PR title and description. They are built from [kubernetes-sigs/kubebuilder-release-tools](https://github.com/kubernetes-sigs/kubebuilder-release-tools/). In Kubebuilder, we have been using this project via the GitHub action [.github/workflows/verify.yml](.github/workflows/verify.yml) and not the image, see: ```yaml verify: name: Verify PR contents runs-on: ubuntu-latest steps: - name: Verifier action id: verifier uses: kubernetes-sigs/kubebuilder-release-tools@v0.1.1 with: github_token: ${{ secrets.GITHUB_TOKEN }} ``` However, the image should still be built and maintained since other projects under the org might be using them. [kubebuilder-release-tools]: https://github.com/kubernetes-sigs/kubebuilder-release-tools [release-notes-generation]: https://github.com/kubernetes-sigs/kubebuilder-release-tools/blob/master/README.md#release-notes-generation [release-process]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/VERSIONING.md#releasing ================================================ FILE: SECURITY_CONTACTS ================================================ # Defined below are the security contacts for this repo. # # They are the contact point for the Product Security Team to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://github.com/kubernetes/sig-release/blob/master/security-release-process-documentation/security-release-process.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ camilamacedo86 varshaprasad96 ================================================ FILE: VERSIONING.md ================================================ # Versioning and Releasing for Kubebuilder We (mostly) follow the [common Kubebuilder versioning guidelines][guidelines], and use the corresponding tooling and PR process described there. For the purposes of the aforementioned guidelines, Kubebuilder counts as a "CLI project". [guidelines]: https://sigs.k8s.io/kubebuilder-release-tools/VERSIONING.md ## Compatibility Note that we generally do not support older release branches, except in extreme circumstances. Bear in mind that changes to scaffolding generally constitute breaking changes -- see [below](#understanding-the-versions) for more details. ## Releasing When releasing, you'll need to: - to update references in [the build directory](build/) to the latest version of the [envtest tools](#tools-releases) **before tagging the release.** - reset the book branch: see [below](#book-releases) You may also want to check that the book is generating the marker docs off the latest controller-tools release. That info is stored in [docs/book/install-and-build.sh](/docs/book/install-and-build.sh). ## Book Releases The book's main version (https://book.kubebuilder.io) is published off of the [book-v3][book-branch] (a version built off the main branch can be found at https://master.book.kubebuilder.io). Docs changes that aren't specific to a new feature should be cherry-picked to the aforementioned branch to get them to be published. The cherry-picks will automatically be published to the book once their PR merges. **When you publish a Kubebuilder release**, be sure to also submit a PR that merges the main branch into [book-v3][book-branch], so that it describes the latest changes in the new release. [book-branch]: https://github.com/kubernetes-sigs/kubebuilder/tree/tools-releases ## Tools Releases In order to update the [envtest tools][envtest-ref], you'll need to do an update to the [tools-releases branch][tools-branch]. Simply submit a PR against that branch that changes all references to the current version to the desired next version. Once the PR is merged, Google Cloud Build will take care of building and publishing the artifacts. [envtest-ref]: https://book.kubebuilder.io/reference/artifacts.html [tools-branch]: https://github.com/kubernetes-sigs/kubebuilder/tree/tools-releases [kb-releases]:https://github.com/kubernetes-sigs/kubebuilder/releases [cli-plugins-versioning]:docs/book/src/plugins/extending#plugin-versioning ================================================ FILE: build/.goreleaser.yml ================================================ # Copyright 2020 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This is a GoReleaser configuration file for Kubebuilder release. # Make sure to check the documentation at http://goreleaser.com # Global environment variables that are needed for hooks and builds. version: 2 env: - GO111MODULE=on # Hooks to run before any build is run. before: hooks: - go mod download # Build a binary for each target in targets. builds: - id: kubebuilder binary: kubebuilder mod_timestamp: "{{ .CommitTimestamp }}" targets: - linux_amd64 - linux_arm64 - linux_ppc64le - linux_s390x - darwin_amd64 - darwin_arm64 env: - CGO_ENABLED=0 # Only binaries of the form "kubebuilder_${goos}_${goarch}" will be released. archives: - formats: ['binary'] # Setting name_template correctly maps checksums to binary names. name_template: "{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" # Checksum all binaries. checksum: name_template: "checksums.txt" # kubebuilder uses a custom changelog, so leave this empty. changelog: # github.com/kubernetes-sigs/kubebuilder release: github: owner: kubernetes-sigs name: kubebuilder # Add the SBOM configuration at the end to generate SBOM files sboms: - id: kubebuilder-sbom artifacts: binary cmd: syft args: ["$artifact", "--output", "cyclonedx-json=$document"] documents: - "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}.cyclonedx.sbom.json" ================================================ FILE: code-of-conduct.md ================================================ # Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) ================================================ FILE: designs/README.md ================================================ Designs ======= These are design documents for changes to Kubebuilder (and cross-repository changes for related projects, like controller-runtime and controller-tools). They exist to help document the design processes that go into writing Kubebuilder, but may not be up-to-date (more below). Not all changes to Kubebuilder need a design document -- only major ones. Use your best judgement. When submitting a design document, we encourage having written a proof-of-concept, and it's perfectly acceptable to submit the proof-of-concept PR simultaneously with the design document, as the proof-of-concept process can help iron out wrinkles and can help with the `Example` section of the template. ## Out-of-Date Designs **Kubebuilder documentation (the [book](https://book.kubebuilder.io) and the [GoDoc](https://pkg.go.dev/sigs.k8s.io/controller-runtime?tab=doc)) should be considered the canonical, update-to-date reference and architectural documentation** for Kubebuilder. However, if you see an out-of-date design document, feel free to submit a PR marking it as such, and add an addendum linking to issues documenting why things changed. For example: ```markdown # Out of Date This change is out of date. It turns out curly braces are frustrating to type, so we had to abandon functions entirely, and have users specify custom functionality using strings of Common LISP instead. See #000 for more information. ``` ================================================ FILE: designs/code-generate-image-plugin.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|---| | @camilamacedo86 | 2021-02-14 | Implemented | [deploy-image-plugin-v1-alpha](../docs/book/src/plugins/available/deploy-image-plugin-v1-alpha.md) | # New Plugin (`deploy-image.go.kubebuilder.io/v1beta1`) to generate code ## Summary This proposal defines a new plugin that allows users to get the scaffold with the required code to have a project that will deploy and manage an image on the cluster following the guidelines and what have been considered as good practices. ## Motivation The biggest part of the Kubebuilder users looking for to create a project that will at the end only deploy an image. In this way, one of the mainly motivations of this proposal is to abstract the complexities to achieve this goal and still giving the possibility of users improve and customize their projects according to their requirements. **Note:** This plugin will address requests that has been raised for a while and for many users in the community. Check [here](https://github.com/operator-framework/operator-sdk/pull/2158), for example, a request done in the past for the SDK project which is integrated with Kubebuidler to address the same need. ### Goals - Add a new plugin to generate the code required to deploy and manage an image on the cluster - Promote the best practices by giving examples of common implementations - Make the process of developing operator's projects easier and more agile. - Give flexibility to the users and allow them to change the code according to their needs - Provide examples of code implementations and of the usage of the most common features and reduce the learning curve ### Non-Goals The idea of this proposal is to provide a facility for the users. This plugin can be improved in the future, however, this proposal just covers the basic requirements. In this way, is a non-goal allow extra configurations such as; scaffolding the project using webhooks and the controller covered by tests. ## Proposal Add the new plugin code generated which will scaffold code implementation to deploy the image informed which would like such as; `kubebuilder create api --group=crew --version=v1 --image=myexample:0.0.1 --kind=App --plugins=deploy-image.go.kubebuilder.io/v1beta1` which will: - Add a code implementation that will do the Custom Resource reconciliation and create a Deployment resource for the `--image`; - Add an EnvVar on the manager manifest (`config/manager/manager.yaml`) which will store the image informed and show its possibility to users: ```yaml .. spec: containers: - name: manager env: - name: {{ resource}}-IMAGE value: {{image:tag}} image: controller:latest ... ``` - Add a check into reconcile to ensure that the replicas of the deployment on the cluster are equal the size defined in the CR: ```go // Ensure the deployment size is the same as the spec size := {{ resource }}.Spec.Size if *found.Spec.Replicas != size { found.Spec.Replicas = &size err = r.Update(ctx, found) if err != nil { log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) return ctrl.Result{}, err } // Spec updated - return and requeue return ctrl.Result{Requeue: true}, nil } ``` - Add the watch feature for the Deployment managed by the controller: ```go func (r *{{ resource }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cachev1alpha1.{{ resource }}{}). Owns(&appsv1.Deployment{}). Complete(r) } ``` - Add the RBAC permissions required for the scenario such as: ```go // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete ``` - A status [conditions][conditions] to allow users to check if the deployment occurred successfully or if its errors - Add a [marker][markers] in the spec definition to demonstrate how to use OpenAPI schemas validation such as `+kubebuilder:validation:Minimum=1` - Add the specs on the `_types.go` to generate the CRD/CR sample with default values for `ImagePullPolicy` (`Always`), `ContainerPort` (`80`) and the `Replicas Size` (`3`) - Add a finalizer implementation with TODO for the CR managed by the controller such as described in the SDK doc [Handle Cleanup on Deletion](https://sdk.operatorframework.io/docs/building-operators/golang/advanced-topics/#handle-cleanup-on-deletion) ### User Stories - I am a user, who would like to use a command to scaffold my common need which is to deploy an image of my application, so that I do not need to know exactly how to implement it - I am a user, would like to have a good example code base that uses the common features so that I can easily learn its concepts and have a good starting point to address my needs. - I am as maintainer, would like to have a good example to address the common questions, so that I can easily describe how to implement the projects and/or use the common features. ### Implementation Details/Notes/Constraints **Example of the controller template** ```go // +kubebuilder:rbac:groups=cache.example.com,resources={{ resource.plural }},verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=cache.example.com,resources={{ resource.plural }}/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cache.example.com,resources={{ resource.plural }}/finalizers,verbs=update // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete func (r *{{ resource }}.Reconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.Log.WithValues("{{ resource }}", req.NamespacedName) // Fetch the {{ resource }} instance {{ resource }} := &{{ apiimportalias }}.{{ resource }}{} err := r.Get(ctx, req.NamespacedName, {{ resource }}) if err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue log.Info("{{ resource }} resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get {{ resource }}") return ctrl.Result{}, err } // Check if the deployment already exists, if not create a new one found := &appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: {{ resource }}.Name, Namespace: {{ resource }}.Namespace}, found) if err != nil && errors.IsNotFound(err) { // Define a new deployment dep := r.deploymentFor{{ resource }}({{ resource }}) log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) err = r.Create(ctx, dep) if err != nil { log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) return ctrl.Result{}, err } // Deployment created successfully - return and requeue return ctrl.Result{Requeue: true}, nil } else if err != nil { log.Error(err, "Failed to get Deployment") return ctrl.Result{}, err } // Ensure the deployment size is the same as the spec size := {{ resource }}.Spec.Size if *found.Spec.Replicas != size { found.Spec.Replicas = &size err = r.Update(ctx, found) if err != nil { log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) return ctrl.Result{}, err } // Spec updated - return and requeue return ctrl.Result{Requeue: true}, nil } // TODO: add here code implementation to update/manage the status return ctrl.Result{}, nil } // deploymentFor{{ resource }} returns a {{ resource }} Deployment object func (r *{{ resource }}Reconciler) deploymentFor{{ resource }}(m *{{ apiimportalias }}.{{ resource }}) *appsv1.Deployment { ls := labelsFor{{ resource }}(m.Name) replicas := m.Spec.Size dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: m.Name, Namespace: m.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{ MatchLabels: ls, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: ls, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Image: imageFor{{ resource }}(m.Name), Name: {{ resource }}, ImagePullPolicy: {{ resource }}.Spec.ContainerImagePullPolicy, Command: []string{"{{ resource }}"}, Ports: []corev1.ContainerPort{{ ContainerPort: {{ resource }}.Spec.ContainerPort, Name: "{{ resource }}", }}, }}, }, }, }, } // Set {{ resource }} instance as the owner and controller ctrl.SetControllerReference(m, dep, r.Scheme) return dep } // labelsFor{{ resource }} returns the labels for selecting the resources // belonging to the given {{ resource }} CR name. func labelsFor{{ resource }}(name string) map[string]string { return map[string]string{"type": "{{ resource }}", "{{ resource }}_cr": name} } // imageFor{{ resource }} returns the image for the resources // belonging to the given {{ resource }} CR name. func imageFor{{ resource }}(name string) string { // TODO: this method will return the value of the envvar create to store the image:tag informed } func (r *{{ resource }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cachev1alpha1.{{ resource }}{}). Owns(&appsv1.Deployment{}). Complete(r) } ``` **Example of the spec for the _types.go template** ```go // {{ resource }}Spec defines the desired state of {{ resource }} type {{ resource }}Spec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // +kubebuilder:validation:Minimum=1 // Size defines the number of {{ resource }} instances Size int32 `json:"size,omitempty"` // ImagePullPolicy defines the policy to pull the container images ImagePullPolicy string `json:"image-pull-policy,omitempty"` // ContainerPort specifies the port which will be used by the image container ContainerPort int `json:"container-port,omitempty"` } ``` ## Design Details ### Test Plan To ensure this implementation a new project example should be generated in the [testdata](../testdata/) directory of the project. See the [test/testdata/generate.sh](../test/testadata/generate.sh). Also, we should use this scaffold in the [integration tests](../test/e2e/) to ensure that the data scaffold works on the cluster as expected. ### Graduation Criteria - The new plugin will only support `project-version=3` - The attribute image with the value informed should be added to the resources model in the PROJECT file to let the tool know that the Resource gets done with the common basic code implementation: ```yaml plugins: deploy-image.go.kubebuilder.io/v1beta1: resources: - domain: example.io group: crew kind: Captain version: v1 image: "/: ``` For further information check the definition agreement register in the comment https://github.com/kubernetes-sigs/kubebuilder/issues/1941#issuecomment-778649947. ## Open Questions 1. Should we allow to scaffold the code for an API that is already created for the project? No, at least in the first moment to keep the simplicity. 2. Should we support StatefulSet and Deployments? The idea is we start it by using a Deployment. However, we can improve the feature in follow-ups to support more default types of scaffolds which could be like `kubebuilder create api --group=crew --version=v1 --image=myexample:0.0.1 --kind=App --plugins=deploy-image.go.kubebuilder.io/v1beta1 --type=[deployment|statefulset|webhook]` 3. Could this feature be useful to other languages or is it just valid to Go-based operators? This plugin would is reponsable to scaffold content and files for Go-based operators. In a future, if other language-based operators starts to be supported (either officially or by the community) this plugin could be used as reference to create an equivalent one for their languages. Therefore, it should probably not to be a `subdomain` of `go.kubebuilder.io.` For its integration with SDK, it might be valid for the Ansible-based operators where a new `playbook/role` could be generated as well. However, for example,for the Helm plugin, it might be useless. E.g `deploy-image.ansible.sdk.operatorframework.io/v1beta1` 4. Should we consider creating a separate repo for plugins? In the long term yes. However, see that currently, Kubebuilder does not have too many plugins yet. And then, the preliminary support for plugins was not indeed released. For more info see the [Extensible CLI and Scaffolding Plugins][plugins-phase1-design-doc]. In this way, at this moment, it shows to be a little Premature Optimization. Note that the issue [#2016](https://github.com/kubernetes-sigs/kubebuilder/issues/1378) will check the possibility of the plugins be as separate binaries that can be discovered by the Kubebuilder CLI binary via user-specified plugin file paths. Then, the discussion over the best approach to dealing with many plugins and if they should or not leave in the Kubebuilder repository would be better addressed after that. 5. Is Kubebuilder prepared to receive this implementation already? The [Extensible CLI and Scaffolding Plugins - Phase 1.5](extensible-cli-and-scaffolding-plugins-phase-1-5.md) and issue #1941 are required to be implemented before this proposal. Also, to have a better idea of the proposed solutions made so for the Plugin Ecosystem see the meta issue [#2016](https://github.com/kubernetes-sigs/kubebuilder/issues/2016) [markers]: ../docs/book/src/reference/markers.md [conditions]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties [plugins-phase1-design-doc]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/extensible-cli-and-scaffolding-plugins-phase-1.md ================================================ FILE: designs/crd_version_conversion.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|-------| | @droot | 01/30/2019| implementable | - | # API Versioning in Kubebuilder This document describes high level design and workflow for supporting multiple versions in an API built using Kubebuilder. Multi-version support was added as an alpha feature in kubernetes project in 1.13 release. Here are links to some recommended reading material. * [CRD version Conversion Design Doc](https://github.com/kubernetes/community/blob/3f8bf88a06a114b3984417d6867bb16506c9c71e/contributors/design-proposals/api-machinery/customresource-conversion-webhook.md) * [CRD Webhook Conversion API changes PR](https://github.com/kubernetes/kubernetes/pull/67795/files) * [CRD Webhook Conversion PR](https://github.com/kubernetes/kubernetes/pull/67006) * [Kubecon talk](https://www.youtube.com/watch?v=HsYtMvvzDyI&t=0s&index=100&list=PLj6h78yzYM2PZf9eA7bhWnIh_mK1vyOfU) * [CRD version conversion POC](https://github.com/droot/crd-conversion-example) # Design ## Hub and Spoke The basic concept is that all versions of an object share the storage. So say if you have versions v1, v2 and v3 of a Kind Toy, kubernetes will use one of the versions to persist the object in stable storage i.e. Etcd. User can specify the version to be used for storage in the Custom Resource definition for that API. One can think storage version as the hub and other versions as spoke to visualize the relationship between storage and other versions (as shown below in the diagram). The key thing to note is that conversion between storage and other version should be lossless (round trippable). As shown in the diagram below, v3 is the storage/hub version and v1, v2 and v4 are spoke version. The document uses storage version and hub interchangeably. ![hub and spoke version diagram][version-diagram] So if each spoke version (v1, v2 and v4 in this case) defines conversion function from/to the hub version, then conversion function between the spoke versions (v1, v2, v4) can be derived. For example, for converting an object from v1 to v4, we can convert v1 to v3 (the hub version) and v3 to v4. We will introduce two interfaces in controller-runtime to express the above relationship. ```Go // Hub defines capability to indicate whether a versioned type is a Hub or not. type Hub interface { runtime.Object Hub() } // A versioned type is convertible if it can be converted to/from a hub type. type Convertible interface { runtime.Object ConvertTo(dst Hub) error ConvertFrom(src Hub) error } ``` A spoke type needs to implement Convertible interface. Kubebuilder can scaffold the skeleton for a type when it is created. An example of Convertible implementation: ```Go package v1 func (ej *ExternalJob) ConvertTo(dst conversion.Hub) error { switch t := dst.(type) { case *v3.ExternalJob: jobv3 := dst.(*v3.ExternalJob) jobv3.ObjectMeta = ej.ObjectMeta // conversion implementation // return nil default: return fmt.Errorf("unsupported type %v", t) } } func (ej *ExternalJob) ConvertFrom(src conversion.Hub) error { switch t := src.(type) { case *v3.ExternalJob: jobv3 := src.(*v3.ExternalJob) ej.ObjectMeta = jobv3.ObjectMeta // conversion implementation return nil default: return fmt.Errorf("unsupported type %v", t) } } ``` The storage type v3 needs to implement the Hub interface: ```Go package v3 func (ej *ExternalJob) Hub() {} ``` ## Conversion Webhook Handler Controller-runtime will implement a default conversion handler that can handle conversion requests for any API type. Code snippets below captures high level implementation details of the handler. This handler will be registered with the webhook server by default. ```Go type conversionHandler struct { // scheme which has Go types for the APIs are registered. This will be injected by controller manager. Scheme runtime.Scheme // decoder which will be injected by the webhook server // decoder knows how to decode a conversion request and API objects. Decoder decoder.Decoder } // This is the default handler which will be mounted on the webhook server. func (ch *conversionHandler) Handle(r *http.Request, w http.Response) { // decode the request to converReview request object convertReq := ch.Decode(r.Body) for _, obj := range convertReq.Objects { // decode the incoming object src, gvk, _ := ch.Decoder.Decode(obj.raw) // get target object instance for convertReq.DesiredAPIVersion and gvk.Kind dst, _ := getTargetObject(convertReq.DesiredAPIVersion, gvk.Kind) // this is where conversion between objects happens ch.ConvertObject(src, dst) // append dst to converted object list } // create a conversion response with converted objects } func (ch *conversionHandler) convertObject(src, dst runtime.Object) error { // check if src and dst are of same type, then may be return with error because API server will never invoke this handler for same version. srcIsHub, dstIsHub := isHub(src), isHub(dst) srcIsConvertible, dstIsConvertible := isConvertible(src), isConvertable(dst) if srcIsHub { if dstIsConvertible { return dst.(conversion.Convertable).ConvertFrom(src.(conversion.Hub)) } else { // this is error case, this can be flagged at setup time ? return fmt.Errorf("%T is not convertible to", src) } } if dstIsHub { if srcIsConvertible { return src.(conversion.Convertable).ConvertTo(dst.(conversion.Hub)) } else { // this is error case. return fmt.Errorf("%T is not convertible", src) } } // neither src or dst are Hub, means both of them are spoke, so lets get the hub // version type. hub, err := getHub(scheme, src) if err != nil { return err } // shall we get Hub for dst type as well and ensure hubs are same ? // src and dst needs to be convertible for it to work if !srcIsConvertable || !dstIsConvertable { return fmt.Errorf("%T and %T needs to be both convertible", src, dst) } err = src.(conversion.Convertible).ConvertTo(hub) if err != nil { return fmt.Errorf("%T failed to convert to hub version %T : %w", src, hub, err) } err = dst.(conversion.Convertible).ConvertFrom(hub) if err != nil { return fmt.Errorf("%T failed to convert from hub version %T : %w", dst, hub, err) } return nil } ``` Handler Registration flow will perform following at the startup: * For APIs with hub defined, it can examine if spoke versions implement convertible or not and can abort with error. * It will also be nice if we can detect an API with multiple versions but with no hub defined, but that requires distinguishing between APIs defined in the project vs external. # CRD Generation The tool that generates the CRD manifests lives under controller-tools repo. Currently it generates the manifests for each discovered under ‘pkg/…’ directory in the project by examining the comments (aka annotations) in Go source files. Following annotations will be added to support multi version: ## Storage/Serve annotations: The resource annotation will be extended to indicate storage/serve attributes as shown below. ```Go // ... // +kubebuilder:resource:storage=true,serve=true // … type APIName struct { ... } ``` The default value of *serve* attribute is true. The default value of *storage* attribute will be *true* for single version and *false* for multiple versions to ensure backward compatibility. CRD generation will be extended to support the following: * If multiple versions are detected for an API: * Ensure only one version is marked as storage version. Assume default value of *storage* to be *false* for this case. * Ensure version specific fields such as *OpenAPIValidationSchema, SubResources and AdditionalPrinterColumn* are added per version and omitted from the top level CRD definition. * In case of single version, * Do not use version specific field in CRD spec because users are most likely running with k8s version < 1.13 which doesn’t support version specific specs for *OpenAPIValidationSchema, SubResources and AdditionalPrinterColumn. *This is critical to maintain backward compatibility. * Assume default value for storage attribute to be *true* for this case. The above two requirements will require CRD generation logic to be divided in two phases. In first phase, parse and store CRD information in an internal structure for all versions and then generate the CRD manifest on the basis of multi-version/single-version scenario. ## Conversion Webhook annotations: Webhook annotations will be extended to support conversion webhook fields. ```Go // ... // +kubebuilder:webhook:conversion:.... // ... ``` These annotations would be placed just above the API type definition to associate conversion webhook with an API type. The exact syntax for annotation is yet to be defined, but goal is CRD generation tool to be able to extract information from these annotation to populate the `CustomResourceConversion` struct in CRD definition. The CA bits for webhook configuration will be populated by using annotations on the CRD as per the [design](https://docs.google.com/document/d/1ipTvFBRoe7fuDiz27Csm5Zb6rH0z6LJTuKM8xY3jaUg/edit?ts=5c49094e#heading=h.u7ei2s2van5b). # Kubebuilder CLI: kubebuilder create api --group g1 --version v2 --Kind k1 [--storage] Fields marked in yellow are proposed new fields to the command and reasoning is stated below. * *--storage* flag gives an option to mark a version as storage/hub version. Generally users have one controller per group/kind, we will avoid scaffolding code for controller if we detect that a controller already exists for an API group/kind. # TODO: ## There is more exploration/work is required in the following areas related to API versioning: * Making it easy to write the conversion function itself. * Making it easy to generate tests for conversion functions using fuzzer. * Best practices around rolling out different versions of the API Version History
Version Updated on Description
Draft 01/30/2019 Initial version
1.0 02/27/2019 Updated the design as per POC implementation
[version-diaiagram]: assets/version_diagram.png ================================================ FILE: designs/discontinue_usage_of_kube_rbac_proxy.md ================================================ | Authors | Creation Date | Status | Extra | |-----------------|---------------|---------------|-------| | @camilamacedo86 | 07/04/2024 | Implementable | - | # Discontinue Kube RBAC Proxy in Default Kubebuilder Scaffolding This proposal highlights the need to reassess the usage of [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) in the default scaffold due to the evolving k8s infra and community feedback. Key considerations include the transition to a shared infrastructure requiring all images to be published on [registry.k8s.io][registry.k8s.io], the deprecation of Google Cloud Platform's [Container Registry](https://cloud.google.com/artifact-registry/docs/transition/prepare-gcr-shutdown), and the fact that [kube-rbac-proxy][kube-rbac-proxy] is yet to be part of the Kubernetes ecosystem umbrella. The dependency on a potentially discontinuable Google infrastructure, **which is out of our control**, paired with the challenges of maintaining, building, or promoting [kube-rbac-proxy][kube-rbac-proxy] images, calls for a change. In this document is proposed to replace the [kube-rbac-proxy][kube-rbac-proxy] within [Network Policies][k8s-doc-networkpolicies] follow-up for potentially enhancements to protect the metrics endpoint combined with [cert-manager][cert-manager] and a new a feature introduced in controller-runtime, see [here][cr-pr]. **For the future (when kube-rbac-proxy be part of the k8s umbrella)**, it is proposed the usage of the [Plugins API provided by Kubebuilder](./../docs/book/src/plugins/plugins.md), to create an [external plugin](./../docs/book/src/plugins/creating-plugins.md) to properly integrate the solution with Kubebuilder and provide a helper to allow users to opt-in as they please them. ## Open Questions - 1) [Network Policies][k8s-doc-networkpolicies] is implemented by the cluster’s CNI. Are we confident that all the major CNIs in use support the proposed policy? > Besides [Network Policies][k8s-doc-networkpolicies] being part of the core Kubernetes API, their enforcement relies on the CNI plugin installed in the Kubernetes cluster. While support and implementation details vary among CNIs, the most commonly used ones, such as Calico, Cilium, WeaveNet, and Canal, offer support for NetworkPolicies. > >Also, there was concern in the past because AWS did not support it. However, this changed, >as detailed in their announcement: [Amazon VPC CNI now supports Kubernetes Network Policies](https://aws.amazon.com/blogs/containers/amazon-vpc-cni-now-supports-kubernetes-network-policies/). > >Moreover, under this proposal, users can still disable/enable this option as they please them. - 2) NetworkPolicy is a simple firewall and does not provide `authn/authz` and encryption. > Yes, that's correct. NetworkPolicy acts as a basic firewall for pods within a Kubernetes cluster, controlling traffic > flow at the IP address or port level. However, it doesn't handle authentication (authn), authorization (authz), > or encryption directly like kube-rbac-proxy solution. > > However, if we can combine the cert-manager and the new feature provided > by controller-runtime, we can achieve the same or a superior level of protection > without relying on any extra third-party dependency. - 3) Could not Kubebuilder maintainers use the shared infrastructure to continue building and promoting those images under the new `registry.k8s.io`? > We tried to do that, see [here](https://github.com/kubernetes/test-infra/blob/master/config/jobs/image-pushing/k8s-staging-kubebuilder.yaml) the recipe implemented. > However, it does not work because kube-rbac-proxy is not under the > kubernetes umbrella. Moreover, we experimented with the GitHub Repository as an alternative approach, see the [PR](https://github.com/kubernetes-sigs/kubebuilder/pull/3854) but seems > that we are not allowed to use it. Nevertheless, neither approach sorts out all motivations and requirements > Ideally, Kubebuilder should not be responsible for maintaining and promoting third-party artefacts. - 4) However, is not Kubebuilder also building and promoting the binaries required to be used within [EnvTest](./../docs/book/src/reference/envtest.md) feature implemented in controller-runtime? > Yes, but it also will need to change. Controller-runtime maintainers are looking for solutions to > build those binaries inside its project since it seems part of its domain. This change is likely > to be transparent to the community users. - 5) Could we not use the Controller-Runtime feature [controller-runtime][cr-pr] which enable secure metrics serving over HTTPS? Yes, after some changes are addressed. After we ask for a hand for reviews from skilled auth maintainers and receive feedback, it appears that this configuration needs to align with best practices. See the [issue](https://github.com/kubernetes-sigs/controller-runtime/issues/2781) raised to track this need. - 6) Could we not make [cert-manager][cert-manager] mandatory? > No, we can not. One of the goals of Kubebuilder is to make it easier for new users. So, we cannot make mandatory the usage of a third party as cert-manager for users by default and to only quick-start. > > However, we can make mandatory the usage of [cert-manager][cert-manager] for some specific features like use kube-rbac-proxy or, as it is today, using webhooks, a more advanced and optional option. ## Summary Starting with release `3.15.0`, Kubebuilder will no longer scaffold new projects with [kube-rbac-proxy][kube-rbac-proxy]. Existing users are encouraged to switch to images hosted by the project on [quay.io](https://quay.io/repository/brancz/kube-rbac-proxy?tab=tags&tag=latest) **OR** to adapt their projects to utilize [Network Policies][k8s-doc-networkpolicies], following the updated scaffold guidelines. For project updates, users can manually review scaffold changes or utilize the provided [upgrade assistance helper](https://book.kubebuilder.io/reference/rescaffold). Communications and guidelines would be provided along with the release. ## Motivation - **Infrastructure Reliability Concerns**: Kubebuilder’s reliance on Google's infrastructure, which may be discontinued at their discretion, poses a risk to image availability and project reliability. [Discussion thread](https://kubernetes.slack.com/archives/CCK68P2Q2/p1711914533693319?thread_ts=1711913605.487359&cid=CCK68P2Q2) and issues: https://github.com/kubernetes/k8s.io/issues/2647 and https://github.com/kubernetes-sigs/kubebuilder/issues/3230 - **Registry Changes and Image Availability**: The transition from `gcr.io` to [registry.k8s.io][registry.k8s.io] and the [Container Registry][container-registry-dep] deprecation implies that **all** images provided so far by Kubebuilder [here][kb-images-repo] will unassailable by **April 22, 2025**. [More info][container-registry-dep] and [slack ETA thread][slack-eta-thread] - **Security and Endorsement Concerns**: [kube-rbac-proxy][kube-rbac-proxy] is a process to be part of auth-sig for an extended period, however, it is not there yet. The Kubernetes Auth SIG’s review reveals that kube-rbac-proxy must undergo significant updates to secure an official endorsement and to be supported, highlighting pressing concerns. You can check the ongoing process and changes required by looking at the [project issue](https://github.com/brancz/kube-rbac-proxy/issues/238) - **Evolving User Requirements and Deprecations**: The anticipated requirement for certificate management, potentially necessitating cert-manager, underlines Kubebuilder's aim to simplify setup and reduce third-party dependencies. [More info, see issue #3524](https://github.com/kubernetes-sigs/kubebuilder/issues/3524) - **Aim for a Transparent and Collaborative Infrastructure**: As an open-source project, Kubebuilder strives for a community-transparent infrastructure that allows broader contributions. This goal aligns with our initiative to migrate Kubebuilder CLI release builds from GCP to GitHub Actions and using Go-Releaser see [here](./../build/.goreleaser.yml), or promoting infrastructure managed under the k8s-infra umbrella. - **Community Feedback**: Some community members preferred its removal from the default scaffolding. [Issue 3482](https://github.com/kubernetes-sigs/kubebuilder/issues/3482) - **Enhancing Service Monitor with Proper TLS/Certificate Usage Requested by Community:** [Issue #3657](https://github.com/kubernetes-sigs/kubebuilder/issues/3657). It is achievable with [kube-rbac-proxy][kube-rbac-proxy] OR [Network Policies][k8s-doc-networkpolicies] usage within [cert-manager][cert-manager]. ### Goals - **Maximize Protection for the Metrics Endpoint without relay in third-part(s)**: Aim to provide the highest level of protection achievable for the metrics endpoint without relying on new third-party dependencies or the need to build and promote images from other projects. - **Avoid Breaking Changes**: Ensure that users who generated projects with previous versions can still use the new version with scaffold changes and adapt their project at their convenience. - **Sustainable Project Maintenance**: Ensure all projects scaffolded by Kubebuilder can be maintained and supported by its maintainers. - **Independence from Google Cloud Platform**: Move away from reliance on Google Cloud Platform, considering the potential for unilateral shutdowns. - **Kubernetes Umbrella Compliance**: Cease the promotion or endorsement of solutions not yet endorsed by the Kubernetes umbrella organization, mainly when used and shipped with the workload. - **Promote Use of External Plugins**: Adhere to Kubebuilder's directive to avoid direct third-party integrations, favouring the support of projects through the Kubebuilder API and [external plugins][external-plugins]. This approach empowers users to add or integrate solutions with the Kubebuilder scaffold on their own, ensuring that third-party project maintainers—who are more familiar with their solutions—can maintain and update their integrations, as implementing it following the best practices to use their project, enhancing the user experience. External plugins should reside within third-party repository solutions and remain up-to-date as part of those changes, aligning with their domain of responsibility. - **Flexible Network Policy Usage**: Allow users to opt-out of the default-enabled usage of [Network Policies][k8s-doc-networkpolicies] if they prefer another solution, plan to deploy their solution with a vendor or use a CNI that does not support NetworkPolicies. ### Non-Goals - **Replicate kube-rbac-proxy Features or Protection Level**: It is not a goal to provide the same features or layer of protection as [kube-rbac-proxy][kube-rbac-proxy]. Since [Network Policies][k8s-doc-networkpolicies]operate differently and do not offer the same kind of functionality as [kube-rbac-proxy][kube-rbac-proxy], achieving identical protection levels through [Network Policies][k8s-doc-networkpolicies]alone is not feasible. However, incorporating NetworkPolicies, cert-manager, and/or the features introduced in the [controller-runtime pull request #2407][cr-pr] we are mainly addressing the security concerns that kube-rbac-proxy handles. ## Proposal ### Phase 1: Transition to network policies The immediate action outlined in this proposal is the replacement of [kube-rbac-proxy][kube-rbac-proxy] with Kubernetes API NetworkPolicies. ### Phase 2: Add Cert-Manager as an Optional option to be used with metrics Looking beyond the initial phase, this proposal envisions integrating cert-manager for TLS certificate management and exploring synergies with new features in Controller Runtime, as demonstrated in [PR #2407](https://github.com/kubernetes-sigs/controller-runtime/pull/2407). These enhancements would introduce encrypted communication for metrics endpoints and potentially incorporate authentication mechanisms, significantly elevating the security model employed by projects scaffolded by Kubebuilder. - **cert-manager**: Automates the management and issuance of TLS certificates, facilitating encrypted communication and, when configured with mTLS, adding a layer of authentication. Currently, we leverage cert-manager when webhooks are scaffolded. So, the proposal idea would be to allow users to enable the cert-manager for the metrics such as those provided and required for the webhook feature. However, it MUST be optional. One of the goals of Kubebuilder is to make it easier for new users. Therefore, new users should not need to deal with cert-manager by default or have the need to install it to just a quick start. That would mean, in a follow-up to the [current open PR](https://github.com/kubernetes-sigs/kubebuilder/pull/3853) to address the above `phase 1 - Transition to NetworkPolices`, we aim to introduce a configurable Kustomize patch that will enable patching the ServiceMonitor in `config/prometheus/monitor.yaml` and certificates similar to our existing setup for webhooks. This enhancement will ensure more flexible deployment configurations and enhance the security features of the service monitoring components. Currently, in the `config/default/`, we have implemented patches for cert-manager along with webhooks, as seen in `config/default/kustomization.yaml` ([example](https://github.com/kubernetes-sigs/kubebuilder/blob/bd0876b8132ff66da12d8d8a0fdc701fde00f54b/docs/book/src/component-config-tutorial/testdata/project/config/default/kustomization.yaml#L51-L149)). These patches handle annotations for the cert-manager CA injection across various configurations, like ValidatingWebhookConfiguration, MutatingWebhookConfiguration, and CRDs. For the proposed enhancements, we need to integrate similar configurations for the ServiceMonitor. This involves the creation of a patch file named `metrics_https_patch.yaml`, which will include configurations necessary for enabling HTTPS for the ServiceMonitor. Here's an example of how this configuration might look: ```sh # [METRICS WITH HTTPS] To enable the ServiceMonitor using HTTPS, uncomment the following line # Note that for this to work, you also need to ensure that cert-manager is enabled in your project - path: metrics_https_patch.yaml ``` This patch should apply similar changes as the current webhook patches, targeting necessary updates in the manifest to support HTTPS communication secured by cert-manager certificates. Here is an example of how the `ServiceMonitor` configured to work with cert-manager might look: ```yaml # Prometheus Monitor Service (Metrics) with cert-manager apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project-v4 app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system annotations: cert-manager.io/inject-ca-from: $(NAMESPACE)/controller-manager-certificate spec: endpoints: - path: /metrics port: https scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: # We should recommend ensure that TLS verification is not skipped in production insecureSkipVerify: false caFile: /etc/prometheus/secrets/ca.crt # CA certificate injected by cert-manager certFile: /etc/prometheus/secrets/tls.crt # TLS certificate injected by cert-manager keyFile: /etc/prometheus/secrets/tls.key # TLS private key injected by cert-manager selector: matchLabels: control-plane: controller-manager ``` ### Phase 3: When Controller-Runtime feature is enhanced After we have the [issue](https://github.com/kubernetes-sigs/controller-runtime/issues/2781) addressed, and we plan to use it to protect the endpoint. See, that would mean ensuring that we are either `handle authentication (authn), authorization (authz)`. Examples of its implementation can be found [here](https://github.com/kubernetes-sigs/cluster-api/blob/v1.6.3/util/flags/diagnostics.go#L79-L82). ### Phase 4: When kube-rbac-proxy be accepted under the umbrella Once kube-rbac-proxy is included in the Kubernetes umbrella, Kubebuilder maintainers can support its integration through a [plugin](https://kubebuilder.io/plugins/plugins). We can following up the ongoing process and changes required for the project be accepted by looking at the [project issue](https://github.com/brancz/kube-rbac-proxy/issues/238). This enables a seamless way to incorporate kube-rbac-proxy into Kubebuilder scaffolds, allowing users to run: ```sh kubebuilder init|edit --plugins="kube-rbac-proxy/v1" ``` So that the plugin could use the [plugin/util](../pkg/plugin/util) lib provide to comment (We can add a method like the [UncommentCode](https://github.com/kubernetes-sigs/kubebuilder/blob/72586d386cfbcaecea6321a703d1d7560c521885/pkg/plugin/util/util.go#L102)) the patches in the `config/default/kustomization` and disable the default network policy used within and [replace the code](https://github.com/kubernetes-sigs/kubebuilder/blob/72586d386cfbcaecea6321a703d1d7560c521885/pkg/plugin/util/util.go#L231) in the `main.go` bellow with to not use the controller-runtime feature instead. ```go ctrlOptions := ctrl.Options{ MetricsFilterProvider: filters.WithAuthenticationAndAuthorization, MetricsSecureServing: true, } ``` ### Documentation Updates Each phase of implementation associated with this proposal must include corresponding updates to the documentation. This is essential to ensure end users understand how to enable, configure, and utilize the options effectively. Documentation updates should be completed as part of the pull request to introduce code changes. ### Proof of Concept - **(Phase 1)NetworkPolicies:** https://github.com/kubernetes-sigs/kubebuilder/pull/3853 - Example of Controller-Runtime new feature to protect Metrics Endpoint: https://github.com/sbueringer/controller-runtime-tests/tree/master/metrics-auth ### Risks and Mitigations #### Loss of Previously Promoted Images The transition to the new shared infrastructure for Kubernetes SIG projects has rendered us unable to automatically build and promote images as before. The process only works for projects under the umbrella. However, the k8s-infra maintainers could manually transfer these images to the new [registry.k8s.io][registry.k8s.io] as a "contingent approach". See: [https://explore.ggcr.dev/?repo=gcr.io%2Fk8s-staging-kubebuilder%2Fkube-rbac-proxy](https://explore.ggcr.dev/?repo=registry.k8s.io%2Fkubebuilder%2Fkube-rbac-proxy) To continue using kube-rbac-proxy, users must update their projects to reference images from the new registry. This requires a project update and a new release, ensuring the image references in the `config/default/manager_auth_proxy_patch.yaml` point to a new place. Therefore, the best approach here for those still interested in using kube-rbac-proxy seems to direct them to the images hosted at [quay.io](https://quay.io/repository/brancz/kube-rbac-proxy?tab=tags&tag=latest), which are maintained by the project itself and then, we keep those images in the registry.k8s.io as a "contingent approach". Ensuring that these images will continue to be promoted under any infrastructure available to Kubebuilder is not reliable or achievable for Kubebuilder maintainers. It is definitely out of our control. #### Impact of Google Cloud Platform Kubebuilder project Kubebuilder hasn't received any official notice regarding a shutdown of its project there so far, but there's a proactive move to transition away from Google Cloud Platform services due to factors beyond our control. Open communication with our community is key as we explore alternatives. It's important to note the [Container Registry Deprecation][container-registry-dep] results in users no longer able to consume those images from the current location from **early 2025**, emphasizing the need to shift away from dependent images as soon as possible and communicate it extensively through mailing lists and other channels to ensure community awareness and readiness. ## Alternatives ### Replace the current images `gcr.io/kubebuilder/kube-rbac-proxy` with `registry.k8s.io/kubebuilder/kube-rbac-proxy` The k8s-infra maintainers assist in ensuring these images will not be lost by: - Manually adding them to [gcr.io/k8s-staging-kubebuilder/kube-rbac-proxy](https://explore.ggcr.dev/?repo=gcr.io%2Fk8s-staging-kubebuilder%2Fkube-rbac-proxy) and promoting them via [registry.k8s.io/kubebuilder/kube-rbac-proxy](https://explore.ggcr.dev/?image=registry.k8s.io%2Fkubebuilder%2Fkube-rbac-proxy:v0.16.0). An available option would be to communicate to users to: - a) Replace their registry from `gcr.io/k8s-staging-kubebuilder/kube-rbac-proxy` to `registry.k8s.io/kubebuilder/kube-rbac-proxy` - b) Clearly state in the docs, Kubebuilder scaffolds, and all channels, including email communications, that kube-rbac-proxy is in the process of becoming part of Kubernetes/auth-sig but is not yet there and hence is a "not supported/secure" solution **Cons:** - Kubebuilder would still not be fully compliant with its goals since it would be scaffolding a third-party integration instead of properly endorsing and promoting the usage of external-plugin APIs. - Kubebuilder would still be promoting a solution not deemed secure/safe according to the review by auth-sig maintainers. - We would still need to manually request k8s-infra maintainers to build and promote these images in the new registry manually. - Changes in the manager/project solution delivered in the scaffold have a critical impact. For example, in this case, users will need to change **ALL** projects they support and ensure that their users no longer use their previously released versions. Following this path, when kube-rbac-proxy is accepted under the Kubernetes/auth-sig, they will start to maintain and manage their own images, which means this path will change again, and Kubebuilder maintainers have no control over ensuring that these images will still be available and promoted for a long period. ### Retain kube-rbac-proxy as an Opt-in Feature and move it to an alpha plugin (Unsupported Feature) AND/OR use the project registry This alternative keeps kube-rbac-proxy out of the default scaffolds, offering it as an optional plugin for users who choose to integrate it. Clear communication will be crucial to inform users about the implications of using kube-rbac-proxy. **Cons:** Mainly, all cons added for the above alternative option `Replace the current images gcr.io/kubebuilder/kube-rbac-proxy` with `registry.k8s.io/kubebuilder/kube-rbac-proxy` within the exception that we would make clear that we kubebuilder is unable to manage those images and move the current implementation for the alpha plugin it would maybe make the process to move it from the Kubebuilder repository to `kube-rbac-proxy` an easier process to allow them to work with the external plugin. However, that is a double effort for users and Kubebuilder maintainers to deal with breaking changes resulting from achieving the ultimate go. Therefore, it would make more sense to encourage using external-plugins API and add this option in their repo once, then create these intermediate steps. [kube-rbac-proxy]: https://github.com/brancz/kube-rbac-proxy [external-plugins]: https://kubebuilder.io/plugins/external-plugins [registry.k8s.io]: https://github.com/kubernetes/registry.k8s.io [container-registry-dep]: https://cloud.google.com/artifact-registry/docs/transition/prepare-gcr-shutdown [kb-images-repo]: https://console.cloud.google.com/gcr/images/kubebuilder/GLOBAL/kube-rbac-proxy [slack-eta-thread]: https://kubernetes.slack.com/archives/CCK68P2Q2/p1712622102206909 [cr-pr]: https://github.com/kubernetes-sigs/controller-runtime/pull/2407 [k8s-doc-networkpolicies]: https://kubernetes.io/docs/concepts/services-networking/network-policies/ [cert-manager]:https://cert-manager.io/ ================================================ FILE: designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|-----------------------------------------------------------------| | @adirio | Mar 9, 2021 | Implemented | [Plugins doc](https://book.kubebuilder.io/plugins/plugins.html) | # Extensible CLI and Scaffolding Plugins - Phase 1.5 Continuation of [Extensible CLI and Scaffolding Plugins](./extensible-cli-and-scaffolding-plugins-phase-1.md). ## Goal The goal of this phase is to achieve one of the goals proposed for Phase 2: chaining plugins. Phase 2 includes several other challenging goals, but being able to chain plugins will be beneficial for third-party developers that are using kubebuilder as a library. The other main goal of phase 2, discovering and using external plugins, is out of the scope of this phase, and will be tackled when phase 2 is implemented. ## Table of contents - [Goal](#goal) - [Motivation](#motivation) - [Proposal](#proposal) - [Implementation](#implementation) ## Motivation There are several cases of plugins that want to maintain most of the go plugin functionality and add certain features on top of it, both inside and outside kubebuilder repository: - [Addon pattern](../plugins/addon) - [Operator SDK](https://github.com/operator-framework/operator-sdk/tree/master/internal/plugins/golang) This behavior fits perfectly under Phase 1.5, where plugins could be chained. However, as this feature is not available, the adopted temporal solution is to wrap the base go plugin and perform additional actions after its `Run` method has been executed. This solution faces several issues: - Wrapper plugins are unable to access the data of the wrapped plugins, as they weren't designed for this purpose, and therefore, most of its internal data is non-exported. An example of this inaccessible data would be the `Resource` objects created inside the `create api` and `create webhook` commands. - Wrapper plugins are dependent on their wrapped plugins, and therefore can't be used for other plugins. - Under the hood, subcommands implement a second hidden interface: `RunOptions`, which further accentuates these issues. Plugin chaining solves the aforementioned problems but the current plugin API, and more specifically the `Subcommand` interface, does not support plugin chaining. - The `RunOptions` interface implemented under the hood is not part of the plugin API, and therefore the cli is not able to run post-scaffold logic (implemented in `RunOptions.PostScaffold` method) after all the plugins have scaffolded their part. - `Resource`-related commands can't bind flags like `--group`, `--version` or `--kind` in each plugin, it must be created outside the plugins and then injected into them similar to the approach followed currently for `Config` objects. ## Proposal Design a Plugin API that combines the current [`Subcommand`](../pkg/plugin/interfaces.go) and [`RunOptions`](../pkg/plugins/internal/cmdutil/cmdutil.go) interfaces and enables plugin-chaining. The new `Subcommand` hooks can be split in two different categories: - Initialization hooks - Execution hooks Initialization hooks are run during the dynamic creation of the CLI, which means that they are able to modify the CLI, e.g. providing descriptions and examples for subcommands or binding flags. Execution hooks are run after the CLI is created, and therefore cannot modify the CLI. On the other hand, as they are run during the CLI execution, they have access to user-provided flag values, project configuration, the new API resource or the filesystem abstraction, as opposed to the initialization hooks. Additionally, some of these hooks may be optional, in which case a non-implemented hook will be skipped when it should be called and consider it succeeded. This also allows to create some hooks specific for a certain subcommand call (e.g.: `Resource`-related hooks for the `edit` subcommand are not needed). Different ordering guarantees can be considered: - Hook order guarantee: a hook for a plugin will be called after its previous hooks succeeded. - Steps order guarantee: hooks will be called when all plugins have finished the previous hook. - Plugin order guarantee: same hook for each plugin will be called in the order specified by the plugin position at the plugin chain. All of the hooks will offer plugin order guarantee, as they all modify/update some item so the order of plugins is important. Execution hooks need to guarantee step order, as the items that are being modified in each step (config, resource, and filesystem) are also needed in the following steps. This is not true for initialization hooks that modify items (metadata and flagset) that are only used in their own methods, so they only need to guarantee hook order. Execution hooks will be able to return an error. A specific error can be returned to specify that no further hooks of this plugin should be called, but that the scaffold process should be continued. This enables plugins to exit early, e.g., a plugin that scaffolds some files only for cluster-scoped resources can detect if the resource is cluster-scoped at one of the first execution steps, and therefore, use this error to tell the CLI that no further execution step should be called for itself. ### Initialization hooks #### Update metadata This hook will be used for two purposes. It provides CLI-related metadata to the Subcommand (e.g., command name) and update the subcommands metadata such as the description or examples. - Required/optional - [ ] Required - [x] Optional - Subcommands - [x] Init - [x] Edit - [x] Create API - [x] Create webhook #### Bind flags This hook will allow subcommands to define specific flags. - Required/optional - [ ] Required - [x] Optional - Subcommands - [x] Init - [x] Edit - [x] Create API - [x] Create webhook ### Execution methods #### Inject configuration This hook will be used to inject the `Config` object that the plugin can modify at will. The CLI will create/load/save this configuration object. - Required/optional - [ ] Required - [x] Optional - Subcommands - [x] Init - [x] Edit - [x] Create API - [x] Create webhook #### Inject resource This hook will be used to inject the `Resource` object created by the CLI. - Required/optional - [x] Required - [ ] Optional - Subcommands - [ ] Init - [ ] Edit - [x] Create API - [x] Create webhook #### Pre-scaffold This hook will be used to take actions before the main scaffolding is performed, e.g. validations. NOTE: a filesystem abstraction will be passed to this hook, but it should not be used for scaffolding. - Required/optional - [ ] Required - [x] Optional - Subcommands - [x] Init - [x] Edit - [x] Create API - [x] Create webhook #### Scaffold This hook will be used to perform the main scaffolding. NOTE: a filesystem abstraction will be passed to this hook that must be used for scaffolding. - Required/optional - [x] Required - [ ] Optional - Subcommands - [x] Init - [x] Edit - [x] Create API - [x] Create webhook #### Post-scaffold This hook will be used to take actions after the main scaffolding is performed, e.g. cleanup. NOTE: a filesystem abstraction will **NOT** be passed to this hook, as post-scaffold task do not require it. In case some post-scaffold task requires a filesystem abstraction, it could be added. NOTE 2: the project configuration is saved by the CLI before calling this hook, so changes done to the configuration at this hook will not be persisted. - Required/optional - [ ] Required - [x] Optional - Subcommands - [x] Init - [x] Edit - [x] Create API - [x] Create webhook ### Override plugins for single subcommand calls Defining plugins at initialization and using them for every command call will solve most of the cases. However, there are some cases where a plugin may be wanted just for a certain subcommand call. For example, a project with multiple controllers may want to follow the declarative pattern in only one of their controllers. The other case is also relevant, a project where most of the controllers follow the declarative pattern may need a single controller not to follow it. In order to achieve this, the `--plugins` flag will be allowed in every command call, overriding the value used in its corresponging project initialization call. ### Plugin chain persistence Currently, the project configuration v3 offers two mechanisms for storing plugin-related information. - A layout field (`string`) that is used for plugin resolution on initialized projects. - A plugin field (`map[string]interface{}`) that is used for plugin configuration raw storage. Plugin resolution uses the `layout` field to resolve plugins. In this phase, it has to store a plugin chain and not a single plugin. As this value is stored as a string, comma-separated representation can be used to represent a chain of plugins instead. NOTE: commas are not allowed in the plugin key. While the `plugin` field may seem like a better fit to store the plugin chain, as it can already contain multiple values, there are several issues with this alternative approach: - A map does not provide any order guarantee, and the plugin chain order is relevant. - Some plugins do not store plugin-specific configuration information, e.g. the `go`-plugins. So the absence of a plugin key doesn't mean that the plugin is not part of the plugin chain. - The desire of running a different set of plugins for a single subcommand call has already been mentioned. Some of these out-of-chain plugins may need to store plugin-specific configuration, so the presence of a plugin doesn't mean that is part of the plugin chain. The next project configuration version could consider this new requirements to define the names/types of these two fields. ### Plugin bundle As a side-effect of plugin chaining, the user experience may suffer if they need to provide several plugin keys for the `--plugins` flag. Additionally, this would also mean a user-facing important breaking change. In order to solve this issue, a plugin bundle concept will be introduced. A plugin bundle behaves as a plugin: - It has a name: provided at creation. - It has a version: provided at creation. - It has a list of supported project versions: computed from the common supported project versions of all the plugins in the bundled. Instead of implementing the optional getter methods that return a subcommand, it offers a way to retrieve the list of bundled plugins. This process will be done after plugin resolution. This way, CLIs will be able to define bundles, which will be used in the user-facing API and the plugin resolution process, but later they will be treated as separate plugins offering the maintainability and separation of concerns advantages that smaller plugins have in comparison with bigger monolithic plugins. ## Implementation The following types are used as input/output values of the described hooks: ```go // CLIMetadata is the runtime meta-data of the CLI type CLIMetadata struct { // CommandName is the root command name. CommandName string } // SubcommandMetadata is the runtime meta-data for a subcommand type SubcommandMetadata struct { // Description is a description of what this subcommand does. It is used to display help. Description string // Examples are one or more examples of the command-line usage of this subcommand. It is used to display help. Examples string } type ExitError struct { Plugin string Reason string } func (e ExitError) Error() string { return fmt.Sprintf("plugin %s exit early: %s", e.Plugin, e.Reason) } ``` The described hooks are implemented through the use of the following interfaces. ```go type RequiresCLIMetadata interface { InjectCLIMetadata(CLIMetadata) } type UpdatesSubcommandMetadata interface { UpdateSubcommandMetadata(*SubcommandMetadata) } type HasFlags interface { BindFlags(*pflag.FlagSet) } type RequiresConfig interface { InjectConfig(config.Config) error } type RequiresResource interface { InjectResource(*resource.Resource) error } type HasPreScaffold interface { PreScaffold(machinery.Filesystem) error } type Scaffolder interface { Scaffold(machinery.Filesystem) error } type HasPostScaffold interface { PostScaffold() error } ``` Additional interfaces define the required method for each type of plugin: ```go // InitSubcommand is the specific interface for subcommands returned by init plugins. type InitSubcommand interface { Scaffolder } // EditSubcommand is the specific interface for subcommands returned by edit plugins. type EditSubcommand interface { Scaffolder } // CreateAPISubcommand is the specific interface for subcommands returned by create API plugins. type CreateAPISubcommand interface { RequiresResource Scaffolder } // CreateWebhookSubcommand is the specific interface for subcommands returned by create webhook plugins. type CreateWebhookSubcommand interface { RequiresResource Scaffolder } ``` An additional interface defines the bundle method to return the wrapped plugins: ```go type Bundle interface { Plugin Plugins() []Plugin } ``` ================================================ FILE: designs/extensible-cli-and-scaffolding-plugins-phase-1.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|-----------------------------------------------------------------| | @estroz,@joelanford | Dec 10, 2019 | Implemented | [Plugins doc](https://book.kubebuilder.io/plugins/plugins.html) | # Extensible CLI and Scaffolding Plugins ## Overview I would like for Kubebuilder to become more extensible, such that it could be imported and used as a library in other projects. Specifically, I'm looking for a way to use Kubebuilder's existing CLI and scaffolding for Go projects, but to also be able to augment the Kubebuilder project structure with other custom project types so that I can support the Kubebuilder workflow with non-Go operators (e.g. operator-sdk's Ansible and Helm-based operators). The idea is for Kubebuilder to define one or more plugin interfaces that can be used to drive what the `init`, `create api` and `create webhooks` subcommands do and to add a new `cli` package that other projects can use to integrate out-of-tree plugins with the Kubebuilder CLI in their own projects. ## Related issues and PRs * [#1148](https://github.com/kubernetes-sigs/kubebuilder/pull/1148) * [#1171](https://github.com/kubernetes-sigs/kubebuilder/pull/1171) * Possibly [#1218](https://github.com/kubernetes-sigs/kubebuilder/issues/1218) ## Prototype implementation Barebones plugin refactor: https://github.com/joelanford/kubebuilder-exp Kubebuilder feature branch: https://github.com/kubernetes-sigs/kubebuilder/tree/feature/plugins-part-2-electric-boogaloo ## Plugin interfaces ### Required Each plugin would minimally be required to implement the `Plugin` interface. ```go type Plugin interface { // Version returns the plugin's semantic version, ex. "v1.2.3". // // Note: this version is different from config version. Version() string // Name returns a DNS1123 label string defining the plugin type. // For example, Kubebuilder's main plugin would return "go". // // Plugin names can be fully-qualified, and non-fully-qualified names are // prepended to ".kubebuilder.io" to prevent conflicts. Name() string // SupportedProjectVersions lists all project configuration versions this // plugin supports, ex. []string{"2", "3"}. The returned slice cannot be empty. SupportedProjectVersions() []string } ``` #### Plugin naming Plugin names (returned by `Name()`) must be DNS1123 labels. The returned name may be fully qualified (fq), ex. `go.kubebuilder.io`, or not but internally will always be fq by either appending `.kubebuilder.io` to the name or using an existing qualifier defined by the plugin. FQ names prevent conflicts between plugin names; the plugin runner will ask the user to add a name qualifier to a conflicting plugin. ### Optional Next, a plugin could optionally implement further interfaces to declare its support for specific Kubebuilder subcommands. For example: * `InitPlugin` - to initialize new projects * `CreateAPIPlugin` - to create APIs (and possibly controllers) for existing projects * `CreateWebhookPlugin` - to create webhooks for existing projects Each of these interfaces would follow the same pattern (see the `InitPlugin` interface example below). ```go type InitPluginGetter interface { Plugin // GetInitPlugin returns the underlying InitPlugin interface. GetInitPlugin() InitPlugin } type InitPlugin interface { GenericSubcommand } ``` Each specialized plugin interface can leverage a generic subcommand interface, which prevents duplication of methods while permitting type checking and interface flexibility. A plugin context can be used to preserve default help text in case a plugin does not implement its own. ```go type GenericSubcommand interface { // UpdateContext updates a PluginContext with command-specific help text, like description and examples. // Can be a no-op if default help text is desired. UpdateContext(*PluginContext) // BindFlags binds the plugin's flags to the CLI. This allows each plugin to define its own // command line flags for the kubebuilder subcommand. BindFlags(*pflag.FlagSet) // Run runs the subcommand. Run() error // InjectConfig passes a config to a plugin. The plugin may modify the // config. Initializing, loading, and saving the config is managed by the // cli package. InjectConfig(*config.Config) } type PluginContext struct { // Description is a description of what this subcommand does. It is used to display help. Description string // Examples are one or more examples of the command-line usage // of this plugin's project subcommand support. It is used to display help. Examples string } ``` #### Deprecated Plugins To generically support deprecated project versions, we could also add a `Deprecated` interface that the CLI could use to decide when to print deprecation warnings: ```go // Deprecated is an interface that, if implemented, informs the CLI // that the plugin is deprecated. The CLI uses this to print deprecation // warnings when the plugin is in use. type Deprecated interface { // DeprecationWarning returns a deprecation message that callers // can use to warn users of deprecations DeprecationWarning() string } ``` ## Configuration ### Config version `3-alpha` Any changes that break `PROJECT` file backwards-compatibility require a version bump. This new version will be `3-alpha`, which will eventually be bumped to `3` once the below config changes have stabilized. ### Project file plugin `layout` The `PROJECT` file will specify what base plugin generated the project under a `layout` key. `layout` will have the format: `Plugin.Name() + "/" + Plugin.Version()`. `version` and `layout` have versions with different meanings: `version` is the project config version, while `layout`'s version is the plugin semantic version. The value in `version` will determine that in `layout` by a plugin's supported project versions (via `SupportedProjectVersions()`). Example `PROJECT` file: ```yaml version: "3-alpha" layout: go/v1.0.0 domain: testproject.org repo: github.com/test-inc/testproject resources: - group: crew kind: Captain version: v1 ``` ## CLI To make the above plugin system extensible and usable by other projects, we could add a new CLI package that Kubebuilder (and other projects) could use as their entrypoint. Example Kubebuilder main.go: ```go func main() { c, err := cli.New( cli.WithPlugins( &golangv1.Plugin{}, &golangv2.Plugin{}, ), ) if err != nil { log.Fatal(err) } if err := c.Run(); err != nil { log.Fatal(err) } } ``` Example Operator SDK main.go: ```go func main() { c, err := cli.New( cli.WithCommandName("operator-sdk"), cli.WithDefaultProjectVersion("2"), cli.WithExtraCommands(newCustomCobraCmd()), cli.WithPlugins( &golangv1.Plugin{}, &golangv2.Plugin{}, &helmv1.Plugin{}, &ansiblev1.Plugin{}, ), ) if err != nil { log.Fatal(err) } if err := c.Run(); err != nil { log.Fatal(err) } } ``` ## Comments & Questions ### Cobra Commands **RESOLUTION:** `cobra` will be used directly in Phase 1 since it is a widely used, feature-rich CLI package. This, however unlikely, may change in future phases. As discussed earlier as part of [#1148](https://github.com/kubernetes-sigs/kubebuilder/pull/1148), one goal is to eliminate the use of `cobra.Command` in the exported API of Kubebuilder since that is considered an internal implementation detail. However, at some point, projects that make use of this extensibility will likely want to integrate their own subcommands. In this proposal, `cli.WithExtraCommands()` _DOES_ expose `cobra.Command` to allow callers to pass their own subcommands to the CLI. In [#1148](https://github.com/kubernetes-sigs/kubebuilder/pull/1148), callers would use Kubebuilder's cobra commands to build their CLI. Here, control of the CLI is retained by Kubebuilder, and callers pass their subcommands to Kubebuilder. This has several benefits: 1. Kubebuilder's CLI subcommands are never exposed except via the explicit plugin interface. This allows the Kubebuilder project to re-implement its subcommand internals without worrying about backwards compatibility of consumers of Kubebuilder's CLI. 2. If desired, Kubebuilder could ensure that extra subcommands do not overwrite/reuse the existing Kubebuilder subcommand names. For example, only Kubebuilder gets to define the `init` subcommand 3. The overall binary's help handling is self-contained in Kubebuilder's CLI. Callers don't have to figure out how to have a cohesive help output between the Kubebuilder CLI and their own custom subcommands. With all of that said, even this exposure of `cobra.Command` could be problematic. If Kubebuilder decides in the future to transition to a different CLI framework (or to roll its own) it has to either continue maintaining support for these extra cobra commands passed into it, or it was to break the CLI API. Are there other ideas for how to handle the following requirements? * Eliminate use of cobra in CLI interface * Allow other projects to have custom subcommands * Support cohesive help output ### Other 1. ~Should the `InitPlugin` interface methods be required of all plugins?~ No 2. ~Any other approaches or ideas?~ 3. ~Anything I didn't cover that could use more explanation?~ ================================================ FILE: designs/extensible-cli-and-scaffolding-plugins-phase-2.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|-----------------------------------------------------------------| | @rashmigottipati | Mar 9, 2021 | partial implemented | [Plugins doc](https://book.kubebuilder.io/plugins/plugins.html) | # Extensible CLI and Scaffolding Plugins - Phase 2 ## Overview Plugin [Phase 1.5](https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md) was designed and implemented to allow chaining of plugins. The purpose of Phase 2 plugins is to discover and use external plugins, also referred to as out-of-tree plugins (which can be implemented in any language). Phase 2 achieves both chaining and discovery of external plugins/source code not compiled with the `kubebuilder` CLI binary. By achieving this goal, we could (for example) externalize the optional [declarative plugin](https://github.com/kubernetes-sigs/kubebuilder/tree/master/pkg/plugins/golang/declarative/v1) which means that the CLI would still be able to use it, however, its source code would no longer be required to be inside of the Kubebuilder repository. ### Related issues and PRs * [Feature Request: Plugins Phase 2](https://github.com/kubernetes-sigs/kubebuilder/issues/1378) * [Extensible CLI and Scaffolding Plugins - Phase 1.5](https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/extensible-cli-and-scaffolding-plugins-phase-1-5.md) * [Phase 1.5 Implementation PR](https://github.com/kubernetes-sigs/kubebuilder/pull/2060) * [Plugin Resolution Enhancement Proposal](https://github.com/kubernetes-sigs/kubebuilder/pull/1942) ### Prototype implementation [POC](https://github.com/rashmigottipati/POC-Phase2-Plugins) - Invoke an external python program that simulates a plugin from a go main and pass messages from `kubebuilder` to the plugin and vice-versa using `stdin/stdout/stderr`. ### User Stories * As a plugin developer, I would like to be able to provide external plugins path for the CLI to perform the scaffolds, so that I could take advantage of external initiatives which are implemented using Kubebuilder as a lib and following its standards but are not shipped with its CLI binaries. * As a Kubebuilder maintainer, I would like to support external plugins not maintained by the core project. * For example, once the Phase 2 plugin implementation is completed, some internal plugins can be re-implemented as external plugins removing the necessity to build those plugins in the `kubebuilder` binary. ### Goals * `kubebuilder` is able to discover plugin binaries and run those plugins using the CLI. * Kubebuilder can use the external plugins as well as its own internal ones to do scaffolding. * `kubebuilder` should be able to show plugin specific information via the `--help` flag. * Support for standard streams i.e. `stdin/stdout/stderr` as the only IPC method between `kubebuilder` and plugins. * Kubebuilder library consumers can support chaining and discovery of out-of-tree plugins. ### Non-Goals * Addition of new arbitrary subcommands other than the subcommands that we already support i.e `init`, `create api`, and `create webhook`. * Discovering plugin binaries that are not locally present on the machine (i.e. binary exists in a remote repository). * Providing other options (other than standard streams such as `stdin/stdout/stderr`) for inter-process communication between `kubebuilder` and external plugins. * Other IPC methods may be allowed in the future, although EPs are required for those methods. ### Examples * `kubebuilder create api --plugins=myexternalplugin/v1` * should scaffold files using the external plugin as defined in its implementation of the `create api` method. * `kubebuilder create api --plugins=myexternalplugin/v1,myotherexternalplugin/v2` * should scaffold files using the external plugin as defined in their implementation of the `create api` method (by respecting the plugin chaining order, i.e. in the order of `create api` of v1 and then `create api` of v2 as specified in the layout field in the configuration). * `kubebuilder create api --plugins=myexternalplugin/v1 --help` * should display help information of the plugin which is not shipped in the binary (myexternalplugin/v1 is present outside of the `kubebuilder` binary). * `kubebuilder create api --plugins=go/v3,myexternalplugin/v2` * should create files using the `go/v3` plugin, then pass those files to `myexternalplugin/v2` as defined in its implementation of the `create api` method by respecting the plugin chaining order. ## Proposal ### Discovery of plugin binaries The method [kustomize](https://kubectl.docs.kubernetes.io/guides/extending_kustomize/) uses to discover plugins, by following a GVK path scheme, is the most natural for this use case since plugins must have a group-like name and version. Every plugin gets its own directory constructed using the plugin name and plugin version for the executable to be placed in and `kubebuilder` will search for a plugin binary with the name of the plugin in the `${name}/${version}` directory of the plugin. This information (plugin name and plugin version) is obtained by `kubebuilder` via the value passed to the `--plugins` CLI flag. Once `kubebuilder` successfully locates the plugin, it will run the plugin using the CLI. Every plugin gets its own directory as below. On Linux: ```shell $XDG_CONFIG_HOME/kubebuilder/plugins/${name}/${version} ``` The default value of XDG_CONFIG_HOME is `$HOME/.config`. On OSX: ```shell ~/Library/Application Support/kubebuilder/plugins/${name}/${version} ``` Based on the above directory scheme, let's say that if the value passed to the `--plugins` CLI flag is `myexternalplugin/v1`: * On Linux: * `kubebuilder` will search for the `myexternalplugin` binary in `$XDG_CONFIG_HOME/kubebuilder/plugins/myexternalplugin/v1`, where the base of this path in is the binary name. * On OSX: * Kubebuilder will search for the `myexternalplugin` binary in `$HOME/Library/Application Support/kubebuilder/plugins/myexternalplugin/v1`. Note: If the name is ambiguous, then the qualified name `myexternalplugin.my.domain` would be used, so the path would be `$XDG_CONFIG_HOME/kubebuilder/plugins/my/domain/myexternalplugin/v1` on Linux and `$HOME/Library/Application Support/kubebuilder/plugins/my/domain/myexternalplugin/v1` on OSX. * Pros * `kustomize` which is popular and robust tool, follows this approach in which `apiVersion` and `kind` fields are used to locate the plugin. * This approach enforces naming constraints as the permitted character set must be directory name-compatible following naming rules for both Linux and OSX systems. * The one-plugin-per-directory requirement eases creation of a plugin bundle for sharing. ### What Plugin system should we use I propose we use our own plugin system that passes JSON blobs back and forth across `stdin/stdout/stderr` and make this the only option for now as it's a language-agnostic medium and it is easy to work with in most languages. We came to the conclusion that a kubebuilder-specific plugin library should be written after evaluating plugin libraries such as the [built-in go-plugin library](https://golang.org/pkg/plugin/) and [Hashicorp's plugin library](https://github.com/hashicorp/go-plugin): * The built-in plugin library seems to be more suitable for in-tree plugins rather than out-of-tree plugins and it doesn't offer cross-language support, thereby making it a non-starter. * Hashicorp's go plugin system is more suitable than the built-in go-plugin library as it enables cross-language/platform support. However, it is more suited for long-running plugins as opposed to short-lived plugins and the usage of protobuf could be overkill as we will not be handling 10s of 1000s of deserializations. In the future, if a need arises (for example, users are hitting performance issues), we can then explore the possibility of using the Hashicorp's go plugin library. From a design standpoint, to leave it architecturally open, I propose using a `type` field in the PROJECT file to potentially allow other plugin libraries in the future and make this a separate field in the PROJECT file per plugin; and this field determines how the `universe` will be passed for a given plugin. However, for the sake of simplicity in initial design and not to introduce any breaking changes as Project version 3 would suffice for our needs, this option is out of scope in this proposal. ### Project configuration Currently, the project configuration has two fields to store plugin specific information. * `Layout` field (of type []string) is used for plugin chain resolution on initialized projects. This will be the default if no plugins are specified for a subcommand. * `Plugins` field (of type map[string]interface{}) is used for option plugin configuration that stores configuration information of any plugin. * So, where should external plugins be defined in the configuration? * I propose that the external plugin should get encoded in the project configuration as a part of the `layout` field. * For example, external plugin `myexternalplugin/v2` can be specified through the `--plugins` flag for every subcommand and also be defined in the project configuration in the `layout` field for plugin resolution. Example `PROJECT` file: ```yaml version: "3" domain: testproject.org layout: - go.kubebuilder.io/v3 - myexternalplugin/v2 plugins: myexternalplugin/v2: resources: - domain: testproject.org group: crew kind: Captain version: v2 declarative.go.kubebuilder.io/v1: resources: - domain: testproject.org group: crew kind: FirstMate version: v1 repo: github.com/test-inc/testproject resources: - group: crew kind: Captain version: v1 ``` ### Communication between `kubebuilder` and external plugins * Why do we need communication between `kubebuilder` and external plugins? * The in-tree plugins do not need any inter-process communication as they are the same process, and hence, direct calls are made to the respective functions (also referred as hooks) based on the supported subcommands for an in-tree plugin. As Phase 2 plugins is tackling out-of-tree or external plugins, there's a need for inter-process communication between `kubebuilder` and the external plugin as they are two separate processes/binaries. `kubebuilder` needs to communicate the subcommand that the external plugin should run, and all the arguments received in the CLI request by the user. These arguments contain flags which will have to be directly passed to all plugins in the chain. Additionally, it's important to have context of all the files that were scaffolded until that point especially if there is more than one external plugin in the chain. `kubebuilder` attaches that information in the request, along with the command and arguments. For the external plugin, it would need to communicate the subcommand it ran and the updated file contents information that the external plugin scaffolded to `kubebuilder`. The external plugin would also need to provide its help text if requested by `kubebuilder`. As discussed earlier, standard streams seems to be a desirable IPC method of communication for the use-cases that Phase 2 is trying to solve that involves discovery and chaining of external plugins. * How does `kubebuilder` communicate to external plugins? * Standard streams have three I/O connections: standard input (`stdin`), standard output (`stdout`) and standard error (`stderr`) and they work well with chaining applications, meaning that output stream of one program can be redirected to the input stream of another. * Let's say there are two external plugins in the plugin chain. Below is the sequence of how `kubebuilder` communicates to the plugins `myfirstexternalplugin/v1` and `mysecondexternalplugin/v1`. ![Kubebuilder to external plugins sequence diagram](https://github.com/rashmigottipati/POC-Phase2-Plugins/blob/main/docs/externalplugins-sequence-diagram.png) * What should be passed between `kubebuilder` and an external plugin? Message passing between `kubebuilder` and the external plugin will occur through a request / response mechanism. The `PluginRequest` will contain information that `kubebuilder` sends *to* the external plugin. The `PluginResponse` will contain information that `kubebuilder` receives *from* the external plugin. The following scenarios show what `kubebuilder` will send/receive to the external plugin: * `kubebuilder` to external plugin: * `kubebuilder` constructs a `PluginRequest` that contains the `Command` (such as `init`, `create api`, or `create webhook`), `Args` containing all the raw flags from the CLI request and license boilerplate without comment delimiters, and an empty `Universe` that contains the current virtual state of file contents that are not written to disk yet. `kubebuilder` writes the `PluginRequest` through `stdin`. * External plugin to `kubebuilder`: * The plugin reads the `PluginRequest` through its `stdin` and processes the request based on the `Command` that was sent. If the `Command` doesn't match what the plugin supports, it writes back an error immediately without any further processing. If the `Command` matches what the plugin supports, it constructs a `PluginResponse` containing the `Command` that was executed by the plugin, and modified `Universe` based on the new files that were scaffolded by the external plugin, `Error` and `ErrorMsg` that add any error information, and writes the `PluginResponse` back to `kubebuilder` through `stdout`. * Note: If `--help` flag is being passed from `kubebuilder` to the external plugin through `PluginRequest`, the plugin attaches its help text information in the `Metadata` field of the `PluginResponse`. Both `PluginRequest` and `PluginResponse` also contain `APIVersion` field to have compatible versioned schemas. * Handling plugin failures across the chain: * If any plugin in the chain fails, the plugin reports errors back through `PluginResponse` to `kubebuilder` and plugin chain execution will be halted, as one plugin may be dependent on the success of another. All the files that were scaffolded already until that point will not be written to disk to prevent a half committed state. ## Implementation Details/Notes/Constraints `PluginRequest` holds all the information `kubebuilder` receives from the CLI and the plugins that were executed before it and the `PluginRequest` will be marshaled into a JSON and sent over `stdin` to the external plugin. `PluginResponse` is what the plugin constructs with the updated universe and sent back to `kubebuilder`. The following structs would be defined on the Kubebuilder side. ```go // PluginRequest contains all information kubebuilder received from the CLI // and plugins executed before it. type PluginRequest struct { // Command contains the command to be executed by the plugin such as init, create api, etc. Command string `json:"command"` // APIVersion defines the versioned schema of the PluginRequest that is encoded and sent from Kubebuilder to plugin. // Initially, this will be marked as alpha (v1alpha1). APIVersion string `json:"apiVersion"` // Args holds the plugin specific arguments that are received from the CLI which are to be passed down to the plugin. Args []string `json:"args"` // Universe represents the modified file contents that gets updated over a series of plugin runs // across the plugin chain. Initially, it starts out as empty. Universe map[string]string `json:"universe"` } // PluginResponse is returned to kubebuilder by the plugin and contains all files // written by the plugin following a certain command. type PluginResponse struct { // Command holds the command that gets executed by the plugin such as init, create api, etc. Command string `json:"command"` // Metadata contains the plugin specific help text that the plugin returns to Kubebuilder when it receives // `--help` flag from Kubebuilder. Metadata plugin.SubcommandMetadata `json:"metadata"` // APIVersion defines the versioned schema of the PluginResponse that will be written back to kubebuilder. // Initially, this will be marked as alpha (v1alpha1). APIVersion string `json:"apiVersion"` // Universe in the PluginResponse represents the updated file contents that was written by the plugin. Universe map[string]string `json:"universe"` // Error is a boolean type that indicates whether there were any errors due to plugin failures. Error bool `json:"error,omitempty"` // ErrorMsg holds the specific error message of plugin failures. ErrorMsg string `json:"error_msg,omitempty"` } ``` The following function handles construction of the `PluginRequest` based on the information `kubebuilder` receives from the CLI and the request is marshaled into JSON. The command to run the external plugin by providing the plugin path will be invoked and `kubebuilder` will send the marshaled `PluginRequest` JSON to the plugin over `stdin`. ```go func (p *ExternalPlugin) runExternalProgram(req PluginRequest) (res PluginResponse, err error) { pluginReq, err := json.Marshal(req) if err != nil { return res, err } cmd := exec.Command(p.Path) cmd.Dir = p.DirContext cmd.Stdin = bytes.NewBuffer(pluginReq) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { fmt.Fprint(os.Stdout, string(out)) return res, err } if json.Unmarshal(out, &res); err != nil { return res, err } return res, nil } ``` On the plugin side, the request JSON will be decoded and depending on what the `Command` in the `PluginRequest` is, the corresponding function to handle `init` or `create api` will be invoked thereby modifying the universe by writing the updated files to it. After `init` or `create api` functions execute successfully, the plugin will write back `PluginResponse` with updated universe and errors (if any) in JSON format through `stdout` to `kubebuilder`. `PluginResponse` also contains error fields `Error` and `ErrorMsg` that the plugin can utilize to add error context if any errors occur. `kubebuilder` receives the command output and decodes into `PluginResponse` struct. This is how message passing will occur between `kubebuilder` and the external plugin. Refer to [POC](https://github.com/rashmigottipati/POC-Phase2-Plugins) for specifics. ### Simple Example ```shell kubebuilder init --plugins=myexternalplugin/v1 --domain example.com ``` What happens when the above is invoked? ![Kubebuilder to external plugins](https://github.com/rashmigottipati/POC-Phase2-Plugins/blob/main/docs/externalplugins-sequence-diagram-2.png) * `kubebuilder` discovers `myexternalplugin/v1` plugin binary and runs the plugin from the discovered path. * Send `PluginRequest` as a JSON over `stdin` to `myexternalplugin` plugin. `PluginRequest JSON`: ```JSON { "command":"init", "args":["--domain","example.com"], "universe":{} } ``` * `myexternalplugin` plugin parses the `PluginRequest` and based on the `Command` specified in the request i.e `init`, performs the necessary scaffolding. * `myexternalplugin` plugin constructs `PluginResponse` with modified `Universe` that contains the updated file contents and errors if any. * Plugin writes `PluginResponse` to stdout in a JSON format back to `kubebuilder`. * `kubebuilder` receives the command output containing the `PluginResponse` JSON which will be decoded into the `PluginResponse` struct. * `kubebuilder` writes the files in the universe to disk. `PluginResponse JSON`: ```JSON { "command": "init", "universe": { "LICENSE": "Apache 2.0 License\n", "main.py": "..." } } ``` ## Alternatives ### Plugin discovery #### User specified file paths A user will provide a list of file paths for `kubebuilder` to discover the plugins in. We will define a variable `KUBEBUILDER_PLUGINS_DIRS` that will take a list of file paths to search for the plugin name. It will also have a default value to search in, in case no file paths are provided. It will search for the plugin name that was provided to the `--plugins` flag in the CLI. `kubebuilder` will recursively search for all file paths until the plugin name is found and returns the successful match, and if it doesn't exist, it returns an error message that the plugin is not found in the provided file paths. Also use the host system mechanism for PATH separation. * Alternatively, this could be handled in a way that [helm kustomize plugin](https://helm.sh/docs/topics/advanced/#post-rendering) discovers the plugin based on the non-existence of a separator in the path provided, in which case `kubebuilder` will search in `$PATH`, otherwise resolve any relative paths to a fully qualified path. * Pros * This provides flexibility for the user to specify the file paths that the plugin would be placed in and `kubebuilder` could discover the binaries in those user specified file paths. * No constraints on plugin binary naming or directory placements from the Kubebuilder side. * Provides a default value for the plugin directory in case user wants to use that to drop their plugins. #### Prefixed plugin executable names in $PATH Another approach is adding plugin executables with a prefix `kubebuilder-` followed by the plugin name to the PATH variable. This will enable `kubebuilder` to traverse through the PATH looking for the plugin executables starting with the prefix `kubebuilder-` and matching by the plugin name that was provided in the CLI. Furthermore, a check should be added to verify that the match is an executable or not and return an error if it's not an executable. This approach provides a lot of flexibility in terms of plugin discovery as all the user needs to do is to add the plugin executable to the PATH and `kubebuilder` will discover it. * Pros * `kubectl` and `git` follow the same approach for discovering plugins, so there's prior art. * There's a lot of flexibility in just dropping plugin binaries to PATH variable and enabling the discovery without having to enforce any other constraints on the placements of the plugins. * Cons * Enumerating the list of all available plugins might be a bit tough compared to having a single folder with the list of available plugins and having to enumerate those. * These plugin binaries cannot be run in a standalone manner outside of Kubebuilder, so may not be very ideal to add them to the PATH var. ## Open questions * Do we want to support the addition of new arbitrary subcommands other than the subcommands (init, create api, create webhook) that we already support? * Not for the EP or initial implementation, but can revisit later. * Do we need to discover flags by calling the plugin binary or should we have users define them in the project configuration? * Flags will be passed directly to the external plugins as a string. Flag parse errors will be passed back via `PluginResponse`. * What alternatives to stdin/stdout exist and why shouldn't we use them? * Other alternatives exist such as named pipe and sockets, but stdin/stdout seems to be more suitable for our needs. * What happens when two plugins bind the same flag name? Will there be any conflicts? * As mentioned in the implementation details section, flags are passed directly as a string to plugins and the same string will be passed to each plugin in the chain, so all plugins get the same flag set. Errors should not be returned if an unrecognized flag is parsed. * How should we handle environment variables? * We would pass the entire CLI environment to the plugin to permit simple external plugin configuration without jumping through hoops. * Should the API version be a part of the plugin request spec? * It would be nice to encode APIVersion for `PluginRequest` and `PluginResponse` so the initial schemas can be marked as `v1alpha1`. ================================================ FILE: designs/helm-chart-autogenerate-plugin.md ================================================ | Authors | Creation Date | Status | Extra | |----------------------------------------|---------------|--------------|-------| | @dashanji,@camilamacedo86,@LCaparelli | Sep, 2023 | Implemented | - | # New Plugin to allow project distribution via helm charts This proposal aims to introduce an optional mechanism that allows users to generate a Helm Chart from their Kubebuilder-scaffolded project. This will enable them to effectively package and distribute their solutions. To achieve this goal, we are proposing a new native Kubebuilder [plugin](https://book.kubebuilder.io/plugins/plugins) (i.e., `helm-chart/v1-alpha`) which will provide the necessary scaffolds. The plugin will function similarly to the existing [Grafana Plugin](https://book.kubebuilder.io/plugins/grafana-v1-alpha), generating or regenerating HelmChart files using the init and edit sub-commands (i.e., `kubebuilder init|edit --plugins helm-chart/v1-alpha`). An alternative solution could be to implement an alpha command, similar to the [helper provided to upgrade projects](https://book.kubebuilder.io/reference/rescaffold) that would provide the HelmChart under the `dist`directory, similar to what is done by [helmify](https://github.com/arttor/helmify). ## Example of Usage **To enable the helm-chart generation when a project is initialized** > kubebuilder init --plugins=`go/v4,helm/v1-alpha` **To enable the helm-chart generation after the project be scaffolded** > kubebuilder edit --plugins=`helm/v1-alpha` > Note that the HelmChart should be scaffold under the `dist/` directory in both scenarios: > ```shell > example-project/ > dist/ > chart/ > ``` **To sync the HelmChart with the latest changes and add the manifests generated** > kubebuilder edit --plugins=`helm/v1-alpha` The above command will be responsible for ensuring that the Helm Chart is properly updated with the latest changes in the project, including the files generated by controller-gen when users run make manifests. ## Open Questions ### 1) How to manage and scaffold the CRDs for the HelmChart? According to [Helm Best Practices for Custom Resource Definitions](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/#method-1-let-helm-do-it-for-you), there are two main methods for handling CRDs: - **Method 1:Let Helm Do It For You:** Place CRDs in the `crds/` directory. Helm installs these CRDs during the initial install but does not manage upgrades or deletions. - **Method 2:Separate Charts:** Place the CRD definition in one chart and the resources using the CRD in another chart. This method requires separate installations for each chart. **Raised Considerations and Concerns** - **Use Helm crd directory** The upgraded chart versions will silently ignore CRDs even if they differ from the installed versions. This could lead to surprising and unexpected behavior. Therefore, Kubebuilder should not encourage or promote this approach. - **Templates Folder**: Moving CRDs to the `templates` folder facilitates upgrades but uninstalls CRDs when the operator is uninstalled. However, it allows users easier manage the CRDs and install them on upgrades. It is a common approach adopted by maintainers but is not considered a good practice by Helm itself. - **Separate Helm Chart for CRDs:** This approach allows control over both CRD and operator versions without deleting CRDs when the operator chart is deleted. Also, follows the HelmChart best practices. Another problem with this approach is to ensure that the CRDs will be applied before the CRs since both will be under the template directory. - **When Webhooks are used:** If a CRD specifies, for example, a conversion webhook, the "API chart" needs to contain the CRDs and the webhook `service/workload`. It would also make sense to include `validating/mutating` webhooks, requiring the scaffolding of separate main modules and image builds for webhooks and controllers which does not shows to be compatible with Kubebuilder Golang scaffold. **Proposed Solution** Follow the same approach adopted by [Cert-Manager](https://cert-manager.io/docs/installation/helm/). Add the CRDs under the `template` directory and have a spec in the `values.yaml` which will define if the CRDs should or not be applied ```shell helm install|upgrade \ myrelease \ --namespace my-namespace \ --set `crds.enabled=true` ``` Also, add another spec to the `values.yaml` to not allow the CRDs be deleted when the helm is uninstalled: ```yaml # START annotations {{- if .Values.crds.keep }} annotations: helm.sh/resource-policy: keep # END annotations {{- end }} ``` Additionally, we might want to scaffold separate charts for the APIs and support both. An example of this approach provided as feedback was [karpenter-provider-aws](https://github.com/aws/karpenter-provider-aws/tree/main/charts). We should either make clear the usage of both supported ways and clarify their limitations. However, the proposed solution would result in the following layout: ``` example-project/ dist/ chart/ example-project-crd/ ├── Chart.yaml ├── templates/ │ ├── _helpers.tpl │ ├── crds/ │ │ └── └── values.yaml example-project/ ├── Chart.yaml ├── templates/ │ ├── _helpers.tpl │ ├── crds/ │ └── │ ├── ... ``` ### 2) How to manage dependencies such as Cert-Manager and Prometheus? Helm charts allow maintainers to define dependencies via the `Chart.yaml` file. However, in the initial version of this plugin at least, we do not need to consider management of dependencies. Adding dependencies such as **Cert-Manager** and **Prometheus** directly in the `Chart.yaml` could introduce issues since these components are intended to be installed only once per cluster. Attempting to manage multiple installations could lead to conflicts and cause unintended behaviors, especially in shared cluster environments. To avoid these issues, the plugin for now will not scaffold this file and will not try to manage it. Instead, users will be responsible for managing these dependencies outside of the generated Helm chart, ensuring they are correctly installed and only installed once in the cluster. ## Motivation Currently, projects scaffolded with Kubebuilder can be distributed via YAML. Users can run `make build-installer IMG=/:tag`, which will generate `dist/install.yaml`. Therefore, its consumers can install the solution by applying this YAML file, such as: `kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml`. However, many adopt solutions require the Helm Chart format, such as FluxCD. Therefore, maintainers are looking to also provide their solutions via Helm Chart. Users currently face the challenges of lacking an officially supported distribution mechanism for Helm Charts. They seek to: - Harness the power of Helm Chart as a package manager for the project, enabling seamless adaptation to diverse deployment environments. - Take advantage of Helm's dependency management capabilities to simplify the installation process of project dependencies, such as cert-manager. - Seamlessly integrate with Helm's ecosystem, including FluxCD, to efficiently manage the project. Consequently, this proposal aims to introduce a method that allows Kubebuilder users to easily distribute their projects through Helm Charts, a strategy that many well-known projects have adopted: - [mongodb](https://artifacthub.io/packages/helm/mongodb-helm-charts/community-operator) - [cert-manager](https://cert-manager.io/v1.6-docs/installation/helm/#1-add-the-jetstack-helm-repository) - [prometheus](https://bitnami.com/stack/prometheus-operator/helm) - [aws-load-balancer-controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller/tree/main/helm/aws-load-balancer-controller) **NOTE:** For further context see the [discussion topic](https://github.com/kubernetes-sigs/kubebuilder/discussions/3074) ## Goals - Allow Kubebuilder users distribute their projects using Helm easily. - Make the best effort to preserve any customizations made to the Helm Charts by the users, which means we will skip syncs in the `values.ymal`. - Stick with Helm layout definitions and externalize into the relevant values-only options to distribute the default scaffold done by Kubebuilder. We should follow https://helm.sh/docs/chart_best_practices. ## Non-Goals - Converting any Kustomize configuration to Helm Charts like [helmify](https://github.com/arttor/helmify) does. - Support the deprecated plugins. This option should be supported from `go/v4` and `kustomize/v2` - Introduce support for Helm in addition to Kustomize, or replace Kustomize with Helm entirely, similar to the approach taken by Operator-SDK, thereby allowing users to utilize Helm Charts to build their Project. - Attend standard practices that deviate from Helm Chart layout, definition, or conventions to workaround its limitations. ## User Stories - As a developer, I want to be able to generate a helm chart from a kustomize directory so that I can distribute the helm chart to my users. Also, I want the generation to be as simple as possible without the need to write any additional duplicate files. - As a user, I want the helm chart can cover all potential configurations when I deploy it on the Kubernetes cluster. - As a platform engineer, I want to be able to manage different versions and configurations of a project across multiple clusters and environments based on the same distribution artifact (Helm Chart), with versioning and dependency locking for supply chain security. ## Implementation Details/Notes/Constraints ### Plugin Layout - **Location and Versioning**: The new plugin should follow Kubebuilder standards and be implemented under `pkg/plugins/optional`. It should be introduced as an alpha version (`v1alpha`), similar to the [Grafana plugin](https://github.com/kubernetes-sigs/kubebuilder/tree/master/pkg/plugins/optional/grafana/v1alpha). - **The data should be tracked in PROJECT File**: Usage of the plugin should be tracked in the `PROJECT` file with the input via flags and options if required. Example entry in the `PROJECT` file: ```yaml ... plugins: helm.go.kubebuilder.io/v1-alpha: options: ## (If ANY) : ``` Ensure that user-provided input is properly tracked, similar to how it's done in other plugins [(see the code in the plugin.go)](https://github.com/kubernetes-sigs/kubebuilder/blob/c058fb95fe0ccd8d2a3147990251ca501df5eb26/pkg/plugins/golang/deploy-image/v1alpha1/plugin.go#L58-L75) and the [(code source to track the data)](https://github.com/kubernetes-sigs/kubebuilder/blob/c058fb95fe0ccd8d2a3147990251ca501df5eb26/pkg/plugins/golang/deploy-image/v1alpha1/api.go#L191-L217) of the deploy-image plugin for reference. **NOTE** We might not need options/flags in the first implementation. However, we should still track the plugin as we do for the Grafana plugin. ### Plugin Implementation Structure Following the structure implementation for the source code of this plugin: ```shell . ├── helm-chart │ └── v1alpha1 │ ├── init.go │ ├── edit.go │ ├── plugin.go │ └── scaffolds │ ├── init.go │ ├── edit.go │ └── internal │ └── templates ``` ### SubCommand implementation For each subCommand we will need to check the resources which are scaffold for each subCommand via the [kustomize](https://kubebuilder.io/plugins/kustomize-v2) plugin and ensure that we will implement the subCommand of the HelmChart plugin to the respective scaffolds as well. #### To Sync the Manifests Created with `controller-gen` Users will need to call the subcommand `edit` passing the plugin to ensure that the Helm chart is properly synced. Therefore, the `PostScaffold` of this command could perform steps such as: - **Run `make manifests`**: Generate the latest CRDs and other manifests. - **Copy the files to Helm chart templates**: - Copy CRDs: `cp config/crd/bases/*.yaml chart/example-project-crd/templates/crds/` - Copy RBAC manifests: `cp config/rbac/*.yaml chart/example-project/templates/rbac/` - Copy webhook configurations: `cp config/webhook/*.yaml chart/example-project/templates/webhook/` - Copy the manager manifest: `cp config/default/manager.yaml chart/example-project/templates/manager/manager.ymal0` - **Replace placeholders with Helm values**: Ensure that customized fields, such as the namespace, are properly replaced accordingly. Example: Replace `name: system` with `{{ .Values.Release.name }}`. This ensures the Helm chart is always up-to-date with the latest manifests generated by Kubebuilder, maintaining consistency with the configured namespace and other customizable fields. We will need to use the utils helpers such as [ReplaceInFile](https://github.com/kubernetes-sigs/kubebuilder/blob/c058fb95fe0ccd8d2a3147990251ca501df5eb26/pkg/plugin/util/util.go#L303-L323) or [EnsureExistAndReplace](https://github.com/kubernetes-sigs/kubebuilder/blob/c058fb95fe0ccd8d2a3147990251ca501df5eb26/pkg/plugin/util/util.go#L276) to achieve this goal. ### HelmChart Values Scaffolded by the Plugin - **Allow values.yaml to be fully re-generated with the flag --force**: By default, the `values.yaml` file should not be overwritten. However, users should have the option to overwrite it using a flag (`--force=true`). This can be implemented in the specific template as done for other plugins: ```go if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { f.IfExistsAction = machinery.Error } ``` **NOTE:** We will evaluate the cases when we implement `webhook.go` and `api.go` for the HelmChart plugin. However, we might use the force flag to replicate the same behavior implemented in the subCommands of the kustomize plugin. For instance, if the flag is used when creating an API, it forces the overwriten of the generated samples. Similarly, if the api subCommand of the HelmChart plugin is called with `--force`, we should replace all samples with the latest versions instead of only adding the new one. - **Helm Chart Templates should have conditions**: Ensure templates install resources based on conditions defined in the `values.yaml`. Example for CRDs: ``` # To install CRDs {{- if .Values.crd.enable }} ... {{- end }} ``` - **Customizable Values**: Set customizable values in the `values.yaml`, such as defining ServiceAccount names, and whether they should be created or not. Furthermore, we should include comments to help end-users understand the source of configurations. Example: ```yaml {{- if .Values.rbac.enable }} apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/name: project-v4 app.kubernetes.io/managed-by: kustomize name: {{ .Values.rbac.serviceAccountName }} namespace: {{ .Release.Namespace }} {{- end }} ``` - **Example of values.yaml**: Following an example to illustrate the expected result of this plugin: ```yaml # Install CRDs under the template crd: enable: false keep: true # Webhook configuration sourced from the `config/webhook` webhook: enabled: true conversion: enabled: true ## RBAC configuration under the `config/rbac` directory rbac: create: true serviceAccountName: "controller-manager" # Cert-manager configuration certmanager: enabled: false issuerName: "letsencrypt-prod" commonName: "example.com" dnsName: "example.com" # Network policy configuration sourced from the `config/network_policy` networkPolicy: enabled: false # Prometheus configuration prometheus: enabled: false # Manager configuration sourced from the `config/manager` manager: replicas: 1 image: repository: "controller" tag: "latest" resources: limits: cpu: 100m memory: 128Mi requests: cpu: 100m memory: 64Mi # Metrics configuration sourced from the `config/metrics` metrics: enabled: true # Leader election configuration sourced from the `config/leader_election` leaderElection: enabled: true role: "leader-election-role" rolebinding: "leader-election-rolebinding" # Controller Manager configuration sourced from the `config/manager` controllerManager: manager: args: - --metrics-bind-address=:8443 - --leader-elect - --health-probe-bind-address=:8081 containerSecurityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL image: repository: controller tag: latest resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi replicas: 1 serviceAccount: annotations: {} # Kubernetes cluster domain configuration kubernetesClusterDomain: cluster.local # Metrics service configuration sourced from the `config/metrics` metricsService: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 type: ClusterIP # Webhook service configuration sourced from the `config/webhook` webhookService: ports: - port: 443 protocol: TCP targetPort: 9443 type: ClusterIP ``` ### Optional configurations should be disabled by default The HelmChart plugin should not scaffold optional options enabled when those are scaffolded as disabled by the default implementation of `kustomize/v2` and consequently the `go/v4` plugin used by default. Example: The dependency on Cert-Manager is disabled by default. ```yaml From config/default/kusyomization.yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager ``` Therefore, by default the `values.yaml` should be scaffolded with: ``` # Cert-manager configuration certmanager: enabled: false ``` ### Layout of the Helm-Chart Following an example of the expected result of this plugin: ```shell example-project/ dist/ chart/ example-project-crd/ ├── Chart.yaml ├── templates/ │ ├── _helpers.tpl │ ├── crds/ │ │ └── └── values.yaml example-project/ ├── Chart.yaml ├── templates/ │ ├── _helpers.tpl │ ├── crds/ │ └── │ ├── certmanager/ │ │ └── certificate.yaml │ ├── manager/ │ │ └── manager.yaml │ ├── network-policy/ │ │ ├── allow-metrics-traffic.yaml │ │ └── allow-webhook-traffic.yaml // Should be added by the plugin subCommand webhook.go │ ├── prometheus/ │ │ └── monitor.yaml │ ├── rbac/ │ │ ├── kind_editor_role.yaml │ │ ├── kind_viewer_role.yaml │ │ ├── leader_election_role.yaml │ │ ├── leader_election_role_binding.yaml │ │ ├── metrics_auth_role.yaml │ │ ├── metrics_auth_role_binding.yaml │ │ ├── metrics_reader_role.yaml │ │ ├── role.yaml │ │ ├── role_binding.yaml │ │ └── service_account.yaml │ ├── samples/ │ │ └── kind_version_admiral.yaml │ ├── webhook/ │ │ ├── manifests.yaml │ │ └── service.yaml └── values.yaml ``` ### Update the README A README.md is scaffold for the projects. (see its implementation [here](https://github.com/kubernetes-sigs/kubebuilder/blob/master/pkg/plugins/golang/v4/scaffolds/internal/templates/readme.go)). Therefore, if the project is scaffold with the HelmChart plugin then, we should update the [Distribution](https://github.com/kubernetes-sigs/kubebuilder/blob/master/testdata/project-v4/README.md#project-distribution) section to add the info and steps over how to keep the HelmChart synced. ### Tests and samples To ensure that the new plugin will work well we will need to: - Implement e2e tests for the plugin. (for reference see the e2e tests for the [DeployImage](https://github.com/kubernetes-sigs/kubebuilder/tree/master/test/e2e/deployimage)) - Ensure that the plugin is scaffold with all samples under the [testdata](https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata) directory (we will need call the plugin in [test/testdata/generate.sh](https://github.com/kubernetes-sigs/kubebuilder/blob/master/test/testdata/generate.sh#L115-L119)) ### Documentation The new plugin should either be properly documented such as the others. For reference see: - [Available Plugins](https://book.kubebuilder.io/plugins/available-plugins) - [DeployImage Plugin](https://book.kubebuilder.io/plugins/deploy-image-plugin-v1-alpha) ## Risks and Mitigations **Difficulty in Maintaining the Solution** Maintaining the solution may prove challenging in the long term, particularly if it does not gain community adoption and, consequently, collaboration. To mitigate this risk, the proposal aims to introduce an optional alpha plugin or to implement it through an alpha command. This approach provides us with greater flexibility to make adjustments or, if necessary, to deprecate the feature without definitively compromising support. ## Proof of Concept In order to prove that would be possible we could refer to the open source tool [helmify](https://github.com/arttor/helmify). ## Drawbacks **Inability to Handle Complex Kubebuilder Scenarios** The proposed plugin may struggle to appropriately handle complex scenarios commonly encountered in Kubebuilder projects, such as intricate webhook configurations. Kubebuilder’s scaffolded projects can have sophisticated webhook setups, and translating these accurately into Helm Charts may prove challenging. This could result in Helm Charts that are not fully reflective of the original project’s functionality or configurations. **Incomplete Generation of Valid and Deployable Helm Charts** The proposed solution may not be capable of generating a fully valid and deployable Helm Chart for all use cases supported by Kubebuilder. Given the diversity and complexity of potential configurations within Kubebuilder projects, there is a risk that the generated Helm Charts may require significant manual intervention to be functional. This drawback undermines the goal of simplifying distribution via Helm Charts and could lead to frustration for users who expect a seamless and automated process. ## Alternatives **Via a new command (Alternative Option)** By running the following command, the plugin will generate a helm chart from the specific kustomize directory and output it to the directory specified by the `--output` flag. ```shell kubebuilder alpha generate-helm-chart --from= --output= ``` The main drawback of this option is that it does not adhere to the Kubebuilder ecosystem. Additionally, we would not take advantage of Kubebuilder library features, such as avoiding overwriting the `values.yaml`. It might also be harder to support and maintain since we would not have the templates as we usually do. Lastly, another con is that it would not allow us to scaffold projects with the plugin enabled and in the future provide further configurations and customizations for this plugin. These configurations would be tracked in the `PROJECT` file, allowing integration with other projects, extensions, and the re-scaffolding of the HelmChart while preserving the inputs provided by the user via plugins flags as it is done for example for the [Deploy Image](https://book.kubebuilder.io/plugins/deploy-image-plugin-v1-alpha) plugin. ================================================ FILE: designs/helper_to_upgrade_projects_by_rescaffolding.md ================================================ | Authors | Creation Date | Status | Extra | |------------------------------------|---------------|-------------|---| | @camilamacedo86,@Kavinjsir,@varshaprasad96 | Feb, 2023 | Implementable | - | Experimental Helper to upgrade projects by re-scaffolding =================== This proposal aims to provide a new alpha command with a helper which would be able to re-scaffold the project from the scratch based on the [PROJECT config][project-config]. ## Example By running a command like following, users would be able to re-scaffold the whole project from the scratch using the current version of KubeBuilder binary available. ```shell kubebuilder alpha generate [OPTIONS] ``` ### Workflows Following some examples of the workflows **To update the project with minor changes provided** See that for each KubeBuilder release the plugins versions used to scaffold the projects might have bug fixes and new incremental features added to the templates which will result in changes to the files that are generated by the tool for new projects. In this case, you previously used the tool to generate the project and now would like to update your project with the latest changes provided for the same plugin version. Therefore, you will need to: - Download and install KubeBuilder binary ( latest / upper release ) - You will run the command in the root directory of your project: `kubebuilder alpha generate` - Then, the command will remove the content of your local directory and re-scaffold the project from the scratch - It will allow you to compare your local branch with the remote branch of your project to re-add the code on top OR if you do not use the flag `--no-backup`, then you can compare the local directory with the copy of your project copied to the path `.backup/project-name/` before the re-scaffold is done. - Therefore, you can run make all and test the final result. You will have after all your project updated. **To update the project with major changes provided** In this case, you are looking for to migrate the project from, for example, `go/v3` to `go/v4`. The steps are very similar to the above ones. However, in this case you need to inform the plugin that you want to use to do the scaffold from scratch `kubebuilder alpha generate --plugins=go/v4`. ## Open Questions N/A ## Summary Therefore, a new command can be designed to load user configs from the [PROJECT config][project-config] file, and run the corresponding kubebuilder subcommands to generate the project based on the new kubebuilder version. Thus, it makes it easier for the users to migrate their operator projects to the new scaffolding. ## Motivation A common scenario is to upgrade the project based on the newer Kubebuilder. The recommended (straightforward) steps are: - a) re-scaffold all files from scratch using the upper version/plugins - b) copy user-defined source code to the new layout The proposed command will automate the process at maximum, therefore helping operator authors with minimizing the manual effort. The main motivation of this proposal is to provide a helper for upgrades and make this process less painful. Examples: - See the discussion [How to regenerate scaffolding?](https://github.com/kubernetes-sigs/kubebuilder/discussions/2864) - From [slack channel By Paul Laffitte](https://kubernetes.slack.com/archives/CAR30FCJZ/p1675166014762669) ### Goals - Help users upgrade their project with the latest changes - Help users re-scaffold projects from scratch based on what was done previously with the tool - Make the upgrade process less painful ### Non-Goals - Change the default layout or how the KubeBuilder CLI works - Deal with customizations or deviations from the proposed layout - Be able to perform the project upgrade to the latest changes without human interactions - Deal and support external plugins - Provide support to older version before having the Project config (Kubebuilder < 3x) and the go/v2 layout which exists to ensure a backwards compatibility with legacy layout provided by Kubebuilder 2x ## Proposal The proposed solution to achieve this goal is to create an alpha command as described in the example section above, see: ```shell kubebuilder alpha generate \ --input-dir= --output-dir= --no-backup --backup-path= --plugins= ``` **Where**: - input-dir: [Optional] If not informed, then, by default, it is the current directory (project directory). If the `PROJECT` file does not exist, it will fail. - output-dir: [Optional] If not informed then, it should be the current repository. - no-backup: [Optional] If not informed then, the current directory should be copied to the path `.backup/project-name` - backup: [Optional] If not informed then, the backup will be copied to the path `.backup/project-name` - plugins: [Optional] If not informed then, it is the same plugin chain available in the layout field - binary: [Optional] If not informed then, the command will use KubeBuilder binary installed globally. > Note that the backup created in the current directory must be prefixed with `.`. Otherwise the tool will not able to perform the scaffold to create a new project from the scratch. This command would mainly perform the following operations: - 1. Check the flags - 2. If the backup flag be used, then check if is a valid path and make a backup of the current project - 3. Copy the whole current directory to `.backup/project-name` - 4. Ensure that the output path is clean. By default it is the current directory project where the project was scaffolded previously and it should be cleaned up before doing the re-scaffold. Only the content under `.backup/project-name` should be kept. - 5. Read the [PROJECT config][project-config] - 6. Re-run all commands using the KubeBuilder binary to recreate the project in the output directory The command should also provide comprehensive help with examples of the proposed workflows. So that, users are able to understand how to use it when run `--help`. ### User Stories **As an Operator author:** - I can re-generate my project from scratch based on the proposed helper, which executes all the commands according to my previous input to the project. That way, I can easily migrate my project to the new layout using the newer CLI/plugin versions, which support the latest changes, bug fixes, and features. - I can regenerate my project from the scratch based on all commands that I used the tool to build my project previously but informing a new init plugin chain, so that I could upgrade my current project to new layout versions and experiment alpha ones. - I would like to re-generate the project from the scratch using the same config provide in the PROJECT file and inform a path to do a backup of my current directory so that I can also use the backup to compare with the new scaffold and add my custom code on top again without the need to compare my local directory and new scaffold with any outside source. **As a Kubebuiler maintainer:** - I can leverage this helper to easily migrate tutorial projects of the Kubebuilder book. - I can leverage on this helper to encourage its users to migrate to upper versions more often, making it easier to maintain the project. ### Implementation Details/Notes/Constraints Note that in the [e2e tests](https://github.com/kubernetes-sigs/kubebuilder/tree/master/test/e2e) the binary is used to do the scaffolds. Also, very similar to the implementation that exist in the integration test KubeBuilder has a code implementation to re-generate the samples used in the docs and add customizations on top, for further information check the [hack/docs](https://github.com/kubernetes-sigs/kubebuilder/tree/master/hack/docs). This subcommand could have a similar implementation that could be used by the tests and this plugin. Note that to run the commands using the binaries we are mainly using the following golang implementation: ```go cmd := exec.Command(t.BinaryName, Options) _, err := t.Run(cmd) ``` ### Risks and Mitigations **Hard to keep the command maintained** A risk to consider is that it would be hard to keep this command maintained because we need to develop specific code operations for each plugin. The mitigation for this problem could be developing a design more generic that could work with all plugins. However, initially a more generic design implementation does not appear to be achievable and would be considered out of the scope of this proposal (no goal). It should to be considered as a second phase of this implementation. Therefore, the current achievable mitigation in place is that KubeBuilder's policy of not providing official support of maintaining and distributing many plugins. ### Proof of Concept All input data is tracked. Also, as described above we have examples of code implementation that uses the binary to scaffold the projects. Therefore, the goal of this project seems very reasonable and achievable. An initial work to try to address this requirement can be checked in this [pull request](https://github.com/kubernetes-sigs/kubebuilder/pull/3022) ## Drawbacks - If the value that feature provides does not pay off the effort to keep it maintained, then we would need to deprecate and remove the feature in the long term. ## Alternatives N/A ## Implementation History The idea of automate the re-scaffold of the project is what motivates us track all input data in to the [project config][project-config] in the past. We also tracked the [issue](https://github.com/kubernetes-sigs/kubebuilder/issues/2068) based on discussion that we have to indeed try to add further specific implementations to do operations per major bumps. For example: To upgrade from go/v3 to go/v4 we know exactly what are the changes in the layout then, we could automate these specific operations as well. However, this first idea is harder yet to be addressed and maintained. ## Future Vision We could use it to do cool future features such as creating a GitHub action which would push-pull requests against the project repositories to help users be updated with, for example, minor changes. By using this command, we might able to git clone the project and to do a new scaffold and then use some [git strategy merge](https://www.geeksforgeeks.org/merge-strategies-in-git/) to result in a PR to purpose the required changes. We probably need to store the CLI tool tag release used to do the scaffold to persuade this idea. So that we can know if the project requires updates or not. [project-config]: https://book.kubebuilder.io/reference/project-config.html ================================================ FILE: designs/integrating-kubebuilder-and-osdk.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|-------| | @joelanford | Sep 6, 2019 | implemented | - | # Integrating Kubebuilder and Operator SDK ## Goal To unite Kubebuilder and Operator SDK around Kubebuilder's project scaffolding, to move Operator SDK's Go operator features upstream, where appropriate, and to join forces on maintaining Kubebuilder so that both Kubebuilder and Operator SDK support the same project structure and command line interface for Go-based operators. ## Background Kubebuilder and [Operator SDK][operator-sdk] are similar projects meant to simplify the process of building a Kubernetes operator (or controller). Both projects make extensive use of the upstream controller-runtime and controller-tools projects, and therefore scaffold similar Go source files and package structures. ## Motivation The Operator SDK and Kubebuilder contributors collaborate on improvements to their shared upstream dependencies, but there is significant overlap between Operator SDK and Kubebuilder related to scaffolding Go operators. Both projects have commands to initialize a new project and add boilerplate implementations of new APIs and controllers. The motivation for integrating Kubebuilder and Operator SDK is that rather than duplicating work related to project scaffolding of Go operators, the projects could work together on one implementation, which would speed up progress and likely result in a more general solution. ## Integration Plan The Kubebuilder and Operator SDK contributors created a [GitHub project][kb-osdk-github-project] to track the work necessary to align the projects. There are three main themes. ### Upstream code from Operator SDK The Operator SDK project contains various features that can be used by Go operator developers regardless of whether the project is based on Kubebuilder or Operator SDK. These features will be upstreamed into `kubebuilder`, `controller-runtime`, and `controller-tools`, where appropriate. These include: * A `DynamicRESTMapper` that enables an operator to dynamically and automatically discover new CRDs added to the cluster after the operator has started * A `GenerationChangedPredicate` that can trigger reconciliation events when a resource's `metadata.generation` field has changed * Flags and helpers that can be used to provide more fine-grained configuration when constructing the default `zap`-based logger The Operator SDK contributors plan to begin conducting all development of Go operator related code in upstream Kubebuilder (and related projects) and to spend more time helping the Kubebuilder contributors maintain these projects. ### Prototypes To make Kubebuilder more extensible, the community has been discussing a proposal to add extension points to Kubebuilder to support different operator patterns. One example of an operator pattern is the [addon pattern][addon-pattern-pr] that uses an existing library to instantiate an opinionated API and controller. More broadly, the idea is to add support for executable plugin-based extensions that can modify Kubebuilder's base scaffolding before files are written to disk so that the project (e.g. Go code, kustomize templates, the project Makefile and Dockerfile) can have customized content provided by an extension. ### Documentation Operator SDK and Kubebuilder currently maintain separate documentation even though a significant chunk of it overlaps. By combining efforts, the SDK contributors will migrate and integrate their Go-based operator documentation upstream into the Kubebuilder documentation and join the Kubebuilder contributors in keeping it up-to-date. [operator-sdk]: https://github.com/operator-framework/operator-sdk [kb-osdk-github-project]: https://github.com/kubernetes-sigs/kubebuilder/projects/7 [addon-pattern-pr]: https://github.com/kubernetes-sigs/kubebuilder/pull/943 ================================================ FILE: designs/simplified-scaffolding.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|---| | @DirectXMan12 | Mar 6, 2019 | Implemented | - | Simplified Builder-Based Scaffolding ==================================== ## Background The current scaffolding in kubebuilder produces a directory structure that looks something like this (compiled artifacts like config omitted for brevity):
`tree -d ./test/project` ```shell $ tree -d ./test/project ./test/project ├── cmd │   └── manager ├── pkg │   ├── apis │   │   ├── creatures │   │   │   └── v2alpha1 │   │   ├── crew │   │   │   └── v1 │   │   ├── policy │   │   │   └── v1beta1 │   │   └── ship │   │   └── v1beta1 │   ├── controller │   │   ├── firstmate │   │   ├── frigate │   │   ├── healthcheckpolicy │   │   ├── kraken │   │   └── namespace │   └── webhook │   └── default_server │   ├── firstmate │   │   └── mutating │   ├── frigate │   │   └── validating │   ├── kraken │   │   └── validating │   └── namespace │   └── mutating └── vendor ```
API packages have a separate file for each API group that creates a SchemeBuilder, a separate file to aggregate those scheme builders together, plus files for types, and the per-group-version scheme builders as well:
`tree ./test/project/pkg/apis` ```shell $ ./test/project/pkg/apis ├── addtoscheme_creatures_v2alpha1.go ├── apis.go ├── creatures │   ├── group.go │   └── v2alpha1 │   ├── doc.go │   ├── kraken_types.go │   ├── kraken_types_test.go │   ├── register.go │   ├── v2alpha1_suite_test.go │   └── zz_generated.deepcopy.go ... ```
Controller packages have a separate file that registers each controller with a global list of controllers, a file that provides functionality to register that list with a manager, as well as a file that constructs the individual controller itself:
`tree ./test/project/pkg/controller` ```shell $ tree ./test/project/pkg/controller ./test/project/pkg/controller ├── add_firstmate.go ├── controller.go ├── firstmate │   ├── firstmate_controller.go │   ├── firstmate_controller_suite_test.go │   └── firstmate_controller_test.go ... ```
## Motivation The current scaffolding in Kubebuilder has two main problems: comprehensibility and dependency passing. ### Complicated Initial Structure While the structure of Kubebuilder projects will likely feel at home for existing Kubernetes contributors (since it matches the structure of Kubernetes itself quite closely), it provides a fairly convoluted experience out of the box. Even for a single controller and API type (without a webhook), it generates 8 API-related files and 5 controller-related files. Of those files, 6 are Kubebuilder-specific glue code, 4 are test setup, and 1 contains standard Kubernetes glue code, leaving only 2 with actual user-edited code. This proliferation of files makes it difficult for users to understand how their code relates to the library, posing some barrier for initial adoption and moving beyond a basic knowledge of functionality to actual understanding of the structure. A common line of questioning amongst newcomers to Kubebuilder includes "where should I put my code that adds new types to a scheme" (and similar questions), which indicates that it's not immediately obvious to these users why the project is structured the way it is. Additionally, we scaffold out API "tests" that test that the API server is able to receive create requests for the objects, but don't encourage modification beyond that. An informal survey seems to indicate that most users don't actually modify these tests (many repositories continue to look like [this](https://github.com/replicatedhq/gatekeeper/blob/3bfe0f7213b6d41abf2df2a6746f3351e709e6ff/pkg/apis/policies/v1alpha2/admissionpolicy_types_test.go)). If we want to help users test that their object's structure is the way they think it is, we're probably better served coming up with a standard "can I create this example YAML file". Furthermore, since the structure is quite convoluted, it makes it more difficult to write examples, as the actual code we care about ends up scattered deep in the folder structure. ### Lack of Builder We introduced the builder pattern for controller construction in controller-runtime ([GoDoc](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/builder?tab=doc#ControllerManagedBy)) as a way to simplify construction of controllers and reduce boilerplate for the common cases of controller construction. Informal feedback from this has been positive, and it enables fairly rapid, clear, and concise construction of controllers (e.g. this [one file controller](https://github.com/DirectXMan12/sample-controller/blob/workshop/main.go) used as a getting started example for a workshop). Current Kubebuilder scaffolding does not take advantage of the builder, leaving generated code using the lower-level constructs which require more understanding of the internals of controller-runtime to comprehend. ### Dependency Passing Woes Another common line of questioning amongst Kubebuilder users is "how to I pass dependencies to my controllers?". This ranges from "how to I pass custom clients for the software I'm running" to "how to I pass configuration from files and flags down to my controllers" (e.g. [kubernete-sigs/kubebuilder#611](https://github.com/kubernetes-sigs/kubebuilder/issues/611) Since reconciler implementations are initialized in `Add` methods with standard signatures, dependencies cannot be passed directly to reconcilers. This has lead to requests for dependency injection in controller-runtime (e.g. [kubernetes-sigs/controller-runtime#102](https://github.com/kubernetes-sigs/controller-runtime/issues/102)), but in most cases, a structure more amicable to passing in the dependencies directly would solve the issue (as noted in [kubernetes-sigs/controller-runtime#182](https://github.com/kubernetes-sigs/controller-runtime/pull/182#issuecomment-442615175)). ## Revised Structure In the revised structure, we use the builder pattern to focus on the "code-refactor-code-refactor" cycle: start out with a simple structure, refactor out as your project becomes more complicated. Users receive a simply scaffolded structure to start. Simple projects can remain relatively simple, and complicated projects can decide to adopt a different structure as they grow. The new scaffold project structure looks something like this (compiled artifacts like config omitted for brevity): ```shell $ tree ./test/project ./test/project ├── main.go ├── controller │   ├── mykind_controller.go │   ├── mykind_controller_test.go │   └── controllers_suite_test.go ├── api │ └── v1 │   └── mykind_types.go │   └── groupversion_info.go └── vendor ``` In this new layout, `main.go` constructs the reconciler: ```go // ... func main() { // ... err := (&controllers.MyReconciler{ MySuperSpecialAppClient: doSomeThingsWithFlags(), }).SetupWithManager(mgr) // ... } ``` while `mykind_controller.go` actually sets up the controller using the reconciler: ```go func (r *MyReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&api.MyAppType{}). Owns(&corev1.Pod{}). Complete(r) } ``` This makes it abundantly clear where to start looking at the code (`main.go` is the defacto standard entry-point for many go programs), and simplifies the levels of hierarchy. Furthermore, since `main.go` actually instantiates an instance of the reconciler, users are able to add custom logic having to do with flags. Notice that we explicitly construct the reconciler in `main.go`, but put the setup logic for the controller details in `mykind_controller.go`. This makes testing easier (see [below](#put-the-controller-setup-code-in-main-go)), but still allows us to pass in dependencies from `main`. ### Why don't we... #### Put the controller setup code in main.go While this is an attractive pattern from a prototyping perspective, it makes it harder to write integration tests, since you can't easily say "run this controller with all its setup in processes". With a separate `SetupWithManager` method associated with reconcile, it becomes fairly easy to setup with a manager. #### Put the types directly under api/, or not have groupversion_info.go These suggestions make it much harder to scaffold out additional versions and kinds. You need to have each version in a separate package, so that type names don't conflict. While we could put scheme registration in with `kind_types.go`, if a project has multiple "significant" Kinds in an API group, it's not immediately clear which file has the scheme registration. #### Use a single types.go file This works fine when you have a single "major" Kind, but quickly grows unwieldy when you have multiple major kinds and end up with a hundreds-of-lines-long `types.go` file (e.g. the `appsv1` API group in core Kubernetes). Splitting out by "major" Kind (`Deployment`, `ReplicaSet`, etc) makes the code organization clearer. #### Change the current scaffold to just make Add a method on the reconciler While this solves the dependency issues (mostly, since you might want to further pass configuration to the setup logic and not just the runtime logic), it does not solve the underlying pedagogical issues around the initial structure burying key logic amidst a sprawl of generated files and directories. ### Making this work with multiple controllers, API versions, API groups, etc #### Versions Most projects will eventually grow multiple API versions. The only wrinkle here is making sure API versions get added to a scheme. This can be solved by adding a specially-marked init function that registration functions get added to (see the example). #### Groups Some projects eventually grow multiple API groups. Presumably, in the case of multiple API groups, the desired hierarchy is: ```shell $ tree ./test/project/api ./test/project/api ├── groupa │   └── v1 │   └── types.go └── groupb    └── v1    └── types.go ``` There are three options here: 1. Scaffold with the more complex API structure (this looks pretty close to what we do today). It doesn't add a ton of complexity, but does bury types deeper in a directory structure. 2. Try to move things and rename references. This takes a lot more effort on the Kubebuilder maintainers' part if we try to rename references across the codebase. Not so much if we force the user to, but that's a poorer experience. 3. Tell users to move things, and scaffold out with the new structure. This is fairly messy for the user. Since growing to multiple API groups seems to be fairly uncommon, it's mostly like safe to take a hybrid approach here -- allow manually specifying the output path, and, when not specified, asking the user to first restructure before running the command. #### Controllers Multiple controllers don't need their own package, but we'd want to scaffold out the builder. We have two options here: 1. Looking for a particular code comment, and appending a new builder after it. This is a bit more complicated for us, but perhaps provides a nicer UX. 2. Simply adding a new controller, and reminding the user to add the builder themselves. This is easier for the maintainers, but perhaps a slightly poorer UX for the users. However, writing out a builder by hand is significantly less complex than adding a controller by hand in the current structure. Option 1 should be fairly simple, since the logic is already needed for registering types to the scheme, and we can always fall back to emitting code for the user to place in manually if we can't find the correct comment. ### Making this work with Existing Kubebuilder Installations Kubebuilder projects currently have a `PROJECT` file that can be used to store information about project settings. We can make use of this to store a "scaffolding version", where we increment versions when making incompatible changes to how the scaffolding works. A missing scaffolding version field implies the version `1`, which uses our current scaffolding semantics. Version `2` uses the semantics proposed here. New projects are scaffolded with `2`, and existing projects check the scaffold version before attempting to add addition API versions, controllers, etc ### Teaching more complicated project structures Some controllers may eventually want more complicated project structures. We should have a section of the book recommending options for when you project gets very complicated. ### Additional Tooling Work * Currently the `api/` package will need a `doc.go` file to make `deepcopy-gen` happy. We should fix this. * Currently, `controller-gen crd` needs the `api` directory to be `pkg/apis//`. We should fix this. ## Example See #000 for an example with multiple stages of code generation (representing the examples is this form is rather complicated, since it involves multiple files). ```shell $ kubebuilder init --domain test.k8s.io $ kubebuilder create api --group mygroup --version v1beta1 --kind MyKind $ kubebuilder create api --group mygroup --version v2beta1 --kind MyKind $ tree . . ├── main.go ├── controller │   ├── mykind_controller.go │   ├── controller_test.go │   └── controllers_suite_test.go ├── api │ ├── v1beta1 │   │ ├── mykind_types.go │   │ └── groupversion_info.go │ └── v1 │   ├── mykind_types.go │   └── groupversion_info.go └── vendor ```
main.go ```go package main import ( "os" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" "k8s.io/apimachinery/pkg/runtime" "my.repo/api/v1beta1" "my.repo/api/v1" "my.repo/controllers" ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { v1beta1.AddToScheme(scheme) v1.AddToScheme(scheme) // +kubebuilder:scaffold:scheme } func main() { ctrl.SetLogger(zap.New(zap.UseDevMode(true))) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{Scheme: scheme}) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } err = (&controllers.MyKindReconciler{ Client: mgr.GetClient(), log: ctrl.Log.WithName("mykind-controller"), }).SetupWithManager(mgr) if err != nil { setupLog.Error(err, "unable to create controller", "controller", "mykind") os.Exit(1) } // +kubebuilder:scaffold:builder setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } ```
mykind_controller.go ```go package controllers import ( "context" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/go-logr/logr" "my.repo/api/v1" ) type MyKindReconciler struct { client.Client log logr.Logger } func (r *MyKindReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { ctx := context.Background() log := r.log.WithValues("mykind", req.NamespacedName) // your logic here return req.Result{}, nil } func (r *MyKindReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(v1.MyKind{}). Complete(r) } ```
`*_types.go` looks nearly identical to the current standard.
groupversion_info.go ```go package v1 import ( "sigs.k8s.io/controller-runtime/pkg/scheme" "k8s.io/apimachinery/pkg/runtime/schema" ) var ( GroupVersion = schema.GroupVersion{Group: "mygroup.test.k8s.io", Version: "v1"} // SchemeBuilder is used to add go types to the GroupVersionKind scheme SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) ```
================================================ FILE: designs/template.md ================================================ | Authors | Creation Date | Status | Extra | |---------------|---------------|-------------|---| | @name | date | Implementable | - | # Title of the Design/Proposal ## Example ## Open Questions [optional] ## Summary ## Motivation ### Goals ### Non-Goals ## Proposal ### User Stories ### Implementation Details/Notes/Constraints [optional] ### Risks and Mitigations ### Proof of Concept [optional] ## Drawbacks ## Alternatives ================================================ FILE: designs/update_action.md ================================================ | Authors | Creation Date | Status | Extra | |-----------------|---------------|-------------|-------| | @camilamacedo86 | 2024-11-07 | Implementable | - | | @vitorfloriano | | Implementable | - | # Proposal: Automating Operator Maintenance: Driving Better Results with Less Overhead ## Introduction Code-generation tools like **Kubebuilder** and **Operator-SDK** have revolutionized cloud-native application development by providing scalable, community-driven frameworks. These tools simplify complexity, accelerate development, and enable developers to create tailored solutions while avoiding common pitfalls, establishing a strong foundation for innovation. However, as these tools evolve to keep up with ecosystem changes and new features, projects risk becoming outdated. Manual updates are time-consuming, error-prone, and create challenges in maintaining security, adopting advancements, and staying aligned with modern standards. This project proposes an **automated solution for Kubebuilder**, with potential applications for similar tools or those built on its foundation. By streamlining maintenance, projects remain modern, secure, and adaptable, fostering growth and innovation across the ecosystem. The automation lets developers focus on what matters most: **building great solutions**. ## Problem Statement Kubebuilder is widely used for developing Kubernetes operators, providing a standardized scaffold. However, as the ecosystem evolves, keeping projects up-to-date presents challenges due to: - **Manual re-scaffolding processes**: These are time-intensive and error-prone. - **Increased risk of outdated configurations**: Leads to security vulnerabilities and incompatibility with modern practices. ## Proposed Solution This proposal introduces a **workflow-based tool** (such as a GitHub Action) that automates updates for Kubebuilder projects. Whenever a new version of Kubebuilder is released, the tool initiates a workflow that: 1. **Detects the new release**. 2. **Generates an updated scaffold**. 3. **Performs a three-way merge to retain customizations**. 4. **Creates a pull request (PR) summarizing the updates** for review and merging. ## Example Usage ### GitHub Actions Workflow: 1. A user creates a project with Kubebuilder `v4.4.3`. 2. When Kubebuilder `v4.5.0` is released, a **pull request** is automatically created. 3. The PR includes scaffold updates while preserving the user’s customizations, allowing easy review and merging. ### Local Tool Usage: 1. A user creates a project with Kubebuilder `v4.4.3` 2. When Kubebuilder `v4.5.0` is released, they run `kubebuilder alpha update` which calls `kubebuilder alpha generate` behind the scenes 3. The tool updates the scaffold and preserves customizations for review and application. 4. In case of conflicts, the tool allows users to resolve them before push a pull request with the changes. ### Handling Merge Conflicts **Local Tool Usage**: If conflicts cannot be resolved automatically, developers can manually address them before completing the update. **GitHub Actions Workflow**: If conflicts arise during the merge, the action will create a pull request and the conflicst will be highlighted in the PR. Developers can then review and resolve them. The PR will contains the default markers: **Example** ```go <<<<<<< HEAD _ = logf.FromContext(ctx) ======= log := log.FromContext(ctx) >>>>>>> original ``` ## Open Questions ### 1. Do we need to create branches to perform the three-way merge,or can we use local temporary directories? > While temporary directories are sufficient for simple three-way merges, branches are better suited for complex scenarios. > They provide history tracking, support collaboration, integrate with CI/CD workflows, and offer more advanced > conflict resolution through Git’s merge command. For these reasons, it seems more appropriate to use branches to ensure > flexibility and maintainability in the merging process. > Furthermore, branches allows a better resolution strategy, > since allows us to use `kubebuilder alpha generate` command to-rescaffold the projects > using the same name directory and provide a better history for the PRs > allowing users to see the changes and have better insights for conflicts > resolution. ### 2. What Git configuration options can facilitate the three-way merge? Several Git configuration options can improve the three-way merge process: ```bash # Show all three versions (base, current, and updated) during conflicts git config --global merge.conflictStyle diff3 # Enable "reuse recorded resolution" to remember and reuse previous conflict resolutions git config --global rerere.enabled true # Increase the rename detection limit to better handle renamed or moved files git config --global merge.renameLimit 999999 ``` These configurations enhance the merging process by improving conflict visibility, reusing resolutions, and providing better file handling, making three-way merges more efficient and developer-friendly. ### 3. If we change Git configurations, can we isolate these changes to avoid affecting the local developer environment when the tool runs locally? It seems that changes can be made using the `-c` flag, which applies the configuration only for the duration of a specific Git command. This ensures that the local developer environment remains unaffected. For example: ``` git -c merge.conflictStyle=diff3 -c rerere.enabled=true merge ``` ### 4. How can we minimize and resolve conflicts effectively during merges? - **Enable Git Features:** - Use `git config --global rerere.enabled true` to reuse previous conflict resolutions. - Configure custom merge drivers for specific file types (e.g., `git config --global merge.<driver>.name "Custom Merge Driver"`). - **Encourage Standardization:** - Adopt a standardized scaffold layout to minimize divergence and reduce conflicts. - **Apply Frequent Updates:** - Regularly update projects to avoid significant drift between the scaffold and customizations. These strategies help minimize conflicts and simplify their resolution during merges. ### 5. How to create the PR with the changes for projects that are monorepos? That means the result of Kubebuilder is not defined in the root dir and might be in other paths. We can define an `--output` directory and a configuration for the GitHub Action where users will define where in their repo the path for the Kubebuilder project is. However, this might be out of scope for the initial version. ### 6. How could AI help us solve conflicts? Are there any available solutions? While AI tools like GitHub Copilot can assist in code generation and provide suggestions, however, it might be risky be 100% dependent on AI for conflict resolution, especially in complex scenarios. Therefore, we might want to use AI as a complementary tool rather than a primary solution. AI can help by: - Providing suggestions for resolving conflicts based on context. - Analyzing code patterns to suggest potential resolutions. - Offering explanations for conflicts and suggesting best practices. - Assisting in summarizing changes. ## Summary ### Workflow Example: 1. A developer creates a project with Kubebuilder `v4.4`. 2. The tooling uses the release of Kubebuilder `v4.5`. 3. The tool: - Regenerates the original base source code for `v4.4` using the `clientVersion` in the `PROJECT` file. - Generates the base source code for `v4.5` 4. A three-way merge integrates the changes into the developer’s project while retaining custom code. 5. The changes now can be packaged into a pull request, summarizing updates and conflicts for the developer’s review. ### Steps: The proposed implementation involves the following steps: 1. **Version Tracking**: - Record the `clientVersion` (initial Kubebuilder version) in the `PROJECT` file. - Use this version as a baseline for updates. - Available in the `PROJECT` file, from [v4.6.0](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/v4.6.0) release onwards. 2. **Scaffold Generation**: - Generate the **original scaffold** using the recorded version. - Generate the **updated scaffold** using the latest Kubebuilder release. 3. **Three-Way Merge**: - Ensure git is configured to handle three-way merges. - Merge the original scaffold, updated scaffold, and the user’s customized project. - Preserve custom code during the merge. 4. **(For Actions) - Pull Request Creation**: - Open a pull request summarizing changes, including details on conflict resolution. - Schedule updates weekly or provide an on-demand option. #### Example Workflow The following example code illustrates the proposed idea but has not been evaluated. This is an early, incomplete draft intended to demonstrate the approach and basic concept. We may want to develop a dedicated command-line tool, such as `kubebuilder alpha update`, to handle tasks like downloading binaries, merging, and updating the scaffold. In this approach, the GitHub Action would simply invoke this tool to manage the update process and open the Pull Request, rather than performing each step directly within the Action itself. ```yaml name: Workflow Auto-Update permissions: contents: write pull-requests: write on: workflow_dispatch: schedule: - cron: "0 0 * * 1" # Every Monday 00:00 UTC jobs: alpha-update: runs-on: ubuntu-latest steps: # 1) Checkout the repository with full history - name: Checkout repository uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 # 2) Install the latest stable Go toolchain - name: Set up Go uses: actions/setup-go@v5 with: go-version: 'stable' # 3) Install Kubebuilder CLI - name: Install Kubebuilder run: | curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)" chmod +x kubebuilder sudo mv kubebuilder /usr/local/bin/ # 4) Extract Kubebuilder version (e.g., v4.6.0) for branch/title/body - name: Get Kubebuilder version id: kb shell: bash run: | RAW="$(kubebuilder version 2>/dev/null || true)" VERSION="$(printf "%s" "$RAW" | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1)" echo "version=${VERSION:-vunknown}" >> "$GITHUB_OUTPUT" # 5) Run kubebuilder alpha update - name: Run kubebuilder alpha update run: | kubebuilder alpha update --force # 6) Restore workflow files so the update doesn't overwrite CI config - name: Restore workflows directory run: | git restore --source=main --staged --worktree .github/workflows git add .github/workflows git commit --amend --no-edit || true # 7) Push to a versioned branch; create PR if missing, otherwise it just updates - name: Push branch and create/update PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: bash run: | set -euo pipefail VERSION="${{ steps.kb.outputs.version }}" PR_BRANCH="kubebuilder-update-to-${VERSION}" # Create or update the branch and push git checkout -B "$PR_BRANCH" git push -u origin "$PR_BRANCH" --force PR_TITLE="chore: update scaffolding to Kubebuilder ${VERSION}" PR_BODY=$'Automated update of Kubebuilder project scaffolding to '"${VERSION}"$'.\n\nMore info: https://github.com/kubernetes-sigs/kubebuilder/releases\n\n :warning: If conflicts arise, resolve them and run:\n```bash\nmake manifests generate fmt vet lint-fix\n```' # Try to create the PR; ignore error only if it already exists if ! gh pr create \ --title "${PR_TITLE}" \ --body "${PR_BODY}" \ --base main \ --head "$PR_BRANCH" then EXISTING="$(gh pr list --state open --head "$PR_BRANCH" --json number --jq '.[0].number' || true)" if [ -n "${EXISTING}" ]; then echo "PR #${EXISTING} already exists for ${PR_BRANCH}, branch updated." else echo "Failed to create PR for ${PR_BRANCH} and no open PR found." exit 1 fi fi ``` ## Motivation A significant challenge faced by Kubebuilder users is keeping their projects up-to-date with the latest scaffolds while preserving customizations. The manual processes required for updates are time-consuming, error-prone, and often discourage users from adopting new versions, leading to outdated and insecure projects. The primary motivation for this proposal is to simplify and automate the process of maintaining Kubebuilder projects. By providing a streamlined workflow for updates, this solution ensures that users can keep their projects aligned with modern standards while retaining their customizations. ### Goals - **Automate Updates**: Detect and apply scaffold updates while preserving customizations. - **Simplify Updates**: Generate pull requests for easy review and merging. - **Provide Local Tooling**: Allow developers to run updates locally with preserved customizations. - **Keep Projects Current**: Ensure alignment with the latest scaffold improvements. - **Minimize Disruptions**: Enable scheduled or on-demand updates. ### Non-Goals - **Automating conflict resolution for heavily customized projects**. - **Automatically merging updates without developer review**. - **Supporting monorepo project layouts or handling repositories that contain more than just the Kubebuilder-generated code**. ## Proposal ### User Stories - **As a Kubebuilder maintainer**, I want to help users keep their projects updated with minimal effort, ensuring they adhere to best practices and maintain alignment with project standards. - **As a user of Kubebuilder**, I want my project to stay up-to-date with the latest scaffold best practices while preserving customizations. - **As a user of Kubebuilder**, I want an easy way to apply updates across multiple repositories, saving time on manual updates. - **As a user of Kubebuilder**, I want to ensure my codebases remain secure and maintainable without excessive manual effort. ### Implementation Details/Notes/Constraints - Introduce a new [Kubebuilder Plugin](https://book.kubebuilder.io/plugins/plugins) that scaffolds the **GitHub Action** based on the POC. This plugin will be released as an **alpha feature**, allowing users to opt-in for automated updates. - The plugin should be added by default in the Golang projects build with Kubebuilder, so new projects can benefit from the automated updates without additional configuration. While it will not be escaffolded by default in tools which extend Kubebuilder such as the Operator-SDK, where the alpha generate and update features cannot be ported or extended. - Documentation should be provided to guide users on how to enable and use the new plugin as the new alpha command - The alpha command update should - provide help and examples of usage - allow users to specify the version of Kubebuilder they want to update to or from to - allow users to specify the path of the project they want to update - allow users to specify the output directory where the updated scaffold should be generated - re-use the existing `kubebuilder alpha generate` command to generate the updated scaffold - The `kubebuilder alpha update` command should be covered with e2e tests to ensure it works as expected and that the generated scaffold is valid and can be built. ## Risks and Mitigations - **Risk**: Frequent conflicts may make the process cumbersome. - *Mitigation*: Provide clear conflict summaries and leverage GitHub preview tools. - **Risk**: High maintenance overhead. - *Mitigation*: Build a dedicated command-line tool (`kubebuilder alpha update`) to streamline updates and minimize complexity. ## Proof of Concept The feasibility of re-scaffolding projects has been demonstrated by the `kubebuilder alpha generate` command. **Command Example:** ```bash kubebuilder alpha generate ``` For more details, refer to the [Alpha Generate Documentation](https://kubebuilder.io/reference/rescaffold). This command allows users to manually re-scaffold a project, to allow users add their code on top. It confirms the technical capability of regenerating and updating scaffolds effectively. This proposal builds upon this foundation by automating the process. The proposed tool would extend this functionality to automatically update projects with new scaffold versions, preserving customizations. The three-way merge approach is a common strategy for integrating changes from multiple sources. It is widely used in version control systems to combine changes from a common ancestor with two sets of modifications. In the context of this proposal, the three-way merge would combine the original scaffold, the updated scaffold, and the user’s custom code seems to be very promising. ### POC Implementation using 3-way merge: Following some POCs done to demonstrate the three-way merge approach where a project was escaffolded with Kubebuilder `v4.5.0` or `v4.5.2` and then updated to `v4.6.0` ```shell ## The following options were passed when merging UPGRADE: git config --global merge.yaml.name "Custom YAML merge" git config --global merge.yaml.driver "yaml-merge %O %A %B" git config merge.conflictStyle diff3 git config rerere.enabled true git config merge.renameLimit 999999 Here are the steps taken: ## On main: git checkout -b ancestor Clean up the ancestor and commit rm -fr * git add . git commit -m "clean up ancestor" ## Bring back the PROJECT file, re-scaffold with v4.5.0, and commit git checkout main -- PROJECT kubebuilder alpha generate git add . git commit -m "alpha generate on ancestor with 4.5.0" ## Then proceed to create the original (ours) branch, bring back the code on main, add and commit: git checkout -b original git checkout main -- . git add . git commit -m "add code back in original" ## Then create the upgrade branch (theirs), run kubebuilder alpha generate with v4.6.0 add and commit: git checkout ancestor git checkout -b upgrade kubebuilder alpha generate git add . git commit -m "alpha generate on upgrade with 4.6.0" ## So now we have the ancestor, the original, and the upgrade branches all set, we can create a branch to commit the merge with the conflict markers: git checkout original git checkout -b merge git merge upgrade git add . git commit -m "Merge with upgrade with conflict markers" ## Now that we have performed the three way merge and commited the conflict markers, we can open a PR against main. ``` As the script: ```bash #!/bin/bash set -euo pipefail # CONFIG — change as needed REPO_PATH="$HOME/go/src/github/camilamacedo86/wordpress-operator" KUBEBUILDER_SRC="$HOME/go/src/sigs.k8s.io/kubebuilder" PROJECT_FILE="PROJECT" echo "📦 Kubebuilder 3-way merge upgrade (v4.5.0 → v4.6.0)" echo "📂 Working in: $REPO_PATH" echo "🧪 Kubebuilder source: $KUBEBUILDER_SRC" cd "$REPO_PATH" # Step 1: Create ancestor branch and clean it up echo "🌱 Creating 'ancestor' branch" git checkout -b ancestor main echo "🧼 Cleaning all files and folders (including dotfiles), except .git and PROJECT" find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {} + git add -A git commit -m "Clean ancestor branch" # Step 2: Install Kubebuilder v4.5.0 and regenerate scaffold echo "⬇️ Installing Kubebuilder v4.5.0" cd "$KUBEBUILDER_SRC" git checkout upstream/release-4.5 make install kubebuilder version cd "$REPO_PATH" echo "📂 Restoring PROJECT file" git checkout main -- "$PROJECT_FILE" kubebuilder alpha generate make manifests generate fmt vet lint-fix git add -A git commit -m "alpha generate on ancestor with v4.5.0" # Step 3: Create original branch with user's code echo "📦 Creating 'original' branch with user code" git checkout -b original git checkout main -- . git add -A git commit -m "Add project code into original" # Step 4: Install Kubebuilder v4.6.0 and scaffold upgrade echo "⬆️ Installing Kubebuilder v4.6.0" cd "$KUBEBUILDER_SRC" git checkout upstream/release-4.6 make install kubebuilder version cd "$REPO_PATH" echo "🌿 Creating 'upgrade' branch from ancestor" git checkout ancestor git checkout -b upgrade echo "🧼 Cleaning all files and folders (including dotfiles), except .git and PROJECT" find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {} + kubebuilder alpha generate make manifests generate fmt vet lint-fix git add -A git commit -m "alpha generate on upgrade with v4.6.0" # Step 5: Merge original into upgrade and preserve conflicts echo "🔀 Creating 'merge' branch from upgrade and merging original" git checkout upgrade git checkout -b merge # Do a non-interactive merge and commit manually echo "🤖 Running non-interactive merge..." set +e git merge --no-edit --no-commit original MERGE_EXIT_CODE=$? set -e # Stage everything and commit with an appropriate message if [ $MERGE_EXIT_CODE -ne 0 ]; then # Manually the alpha generate should out put the info so the person can fix it echo "⚠️ Conflicts occurred." echo "You will need to fix the conflicts manually and run the following commands:" echo "make manifests generate fmt vet lint-fix" echo "⚠️ Conflicts occurred. Keeping conflict markers and committing them." git add -A git commit -m "upgrade has conflicts to be solved" else echo "Merge successful with no conflicts. Running commands" make manifests generate fmt vet lint-fix echo "✅ Merge successful with no conflicts." git add -A git commit -m "upgrade worked without conflicts" fi echo "" echo "📍 You are now on the 'merge' branch." echo "📤 Push with: git push -u origin merge" echo "🔁 Then open a PR to 'main' on GitHub." echo "" ``` ## Drawbacks - **Frequent Conflicts:** Automated updates may often result in conflicts, making the process cumbersome for users. - **Complex Resolutions:** If conflicts are hard to review and resolve, users may find the solution impractical. - **Maintenance Overhead:** The implementation could become too complex for maintainers to develop and support effectively. ## Alternatives - **Manual Update Workflow**: Continue with manual updates where users regenerate and merge changes independently, though this is time-consuming and error-prone. - **Use alpha generate command**: Continue with partially automated updates provided by the alpha generate command. - **Dependabot Integration**: Leverage Dependabot for dependency updates, though this doesn’t fully support scaffold updates and could lead to incomplete upgrades. ================================================ FILE: docs/CONTRIBUTING-ROLES.md ================================================ Contributing Roles ================== ## Direct Code-Related Roles While anyone (who's signed the [CLA and follows the code of conduct](../CONTRIBUTING.md)) is welcome to contribute to the Kubebuilder project, we've got two "formal" roles that carry additional privileges and responsibilities: *reviewer* and *approver*. In a nutshell, reviewers and approvers are officially recognized to make day-to-day and overarching technical decisions within parts of the project, or the project as a whole. We follow a similar set of definitions to the [main Kubernetes project itself][kube-ladder], with slightly looser requirements. As much as possible, we want people to help take on responsibility for the project -- these guidelines are attempts to make it *easier* for this to happen, *not harder*. If you've got any questions, just reach out on Slack to one of the [subproject leads][kb-leads] (called kubebuilder-admins in the `OWNERS_ALIASES` file). ## Prerequisite: Member Anyone who wants to become a reviewer or approver must first be a [member of the Kubernetes project][kube-member]. The aforementioned doc has more details, but the gist is that you must have made a couple of contributions to some part of the Kubernetes project -- *this includes Kubebuilder and related repos*. Then, you need two existing members to sponsor you. **If you've contributed a few times to Kubebuilder, we'll be happy to sponsor you, just ping us on Slack :-)** ## Reviewers Reviewers are recognized as able to provide code reviews for parts of the codebase and are entered into the `reviewers` section of one or more `OWNERS` files. You'll get auto-assigned reviews for your area of the codebase and are generally expected to review for correctness, testing, general code organization, etc. Reviewers may review for design as well, but approvers have the final say on that. Things to look for: - does this code work, and is it written performantly and idiomatically? - is it tested? - is it organized nicely? Is it maintainable? - is it documented? - does it need to be threadsafe? Is it? - Take a glance at the stuff for approvers, if you can. Reviewers' `/lgtm` marks are generally trusted by approvers to means that the code is ready for one last look-over before merging. ### Becoming a Reviewer The criteria for becoming a reviewer are: - Give 5-10 reviews on PRs - Contribute or review 3-5 PRs substantially (i.e. take on the role of the defacto "main" reviewer for the PR, contribute a bugfix or feature, etc) Usually, this will need to occur within a single repository, but if you've worked on a cross-cutting feature, it's ok to count PRs across repositories. Once you meet those criteria, submit yourself as a reviewer in the `OWNERS` file or files that you feel represent your areas of knowledge via a PR to the relevant repository. ## Approvers Approvers provide the final say as to whether a piece of code is merged. Once approvals (`/approve`) are given for each piece of the affected code (and a reviewer or approver has added `/lgtm`), the code will merge. Approvers are responsible for giving the code a final once-over before merge, and do an overall design/API review. Things to look for: - Does the API exposed to the user make sense, and is it easy to use? - Is it backward compatible? - Will it accommodate new changes in the future? - Is it extensible/layer-able (see [DESIGN.md](../DESIGN.md))? - Does it expose a new type from `k8s.io/XYZ`, and, if so, is it worth it? Is that piece well-designed? **For large changes, approvers are responsible for getting reasonable consensus**. With the power to approve such changes comes the responsibility of ensuring that the project as a whole has time to discuss them. ### Becoming an Approver All approvers need to start as reviewers. The criteria for becoming an approver is: - Be a reviewer in the area for a couple of months - Be the "main" reviewer or contributor for 5-10 substantial (bugfixes, features, etc) PRs where approvers did not need to leave substantial additional comments (i.e. where you were acting as a defacto approver). Once you've met those criteria, you can submit yourself as an approver using a PR that edits the relevant `OWNERS` files appropriately. The existing approvers will then approve the change with lazy consensus. If you feel more comfortable asking before submitting the PR, feel free to ping one of the [subproject leads][kb-leads] (called kubebuilder-admins in the `OWNERS_ALIASES` file) on Slack. ## Indirectly Code-Related/Non-Code Roles We're always looking for help with other areas of the project as well, such as: ### Docs Docs contributors are always welcome. Docs folks can also become reviewers/approvers for the book by following the same process above. ### Triage Help to triage our issues is also welcome. Folks doing triage are responsible for using the following commands to mark PRs and issues with one or more labels, and should also feel free to help answer questions: - `/kind {bug|feature|documentation}`: things that are broken/new things/things with lots of words, respectively - `/triage support`: questions, and things that might be bugs but might just be confused about how to use something - `/priority {backlog|important-longterm|important-soon|critical-urgent}`: how soon we need to deal with the thing (if someone wants to/eventually/pretty soon/RIGHT NOW OMG THINGS ARE ON FIRE, respectively) - `/good-first-issue`: this is pretty straightforward to implement, has a clear plan, and clear criteria for being complete - `/help`: this could feasibly still be picked up by someone new-ish, but has some wrinkles or nitty-gritty details that might not make it a good first issue See the [Prow reference](https://prow.k8s.io/command-help) for more details. [kube-ladder]: https://github.com/kubernetes/community/blob/master/community-membership.md "Kubernetes Community Membership" [kube-member]: https://github.com/kubernetes/community/blob/master/community-membership.md#member "Kubernetes Project Member" [kb-leads]: ../OWNERS_ALIASES "Root OWNERS file -- kubebuilder-admins" ================================================ FILE: docs/README.md ================================================ # Running mdBook The kubebuilder book is served using [mdBook](https://github.com/rust-lang-nursery/mdBook). If you want to test changes to the book locally, follow these directions: 1. Follow the instructions at [https://rust-lang.github.io/mdBook/guide/installation.html](https://rust-lang.github.io/mdBook/guide/installation.html) to install mdBook. 2. Make sure [controller-gen](https://pkg.go.dev/sigs.k8s.io/controller-tools/cmd/controller-gen) is installed in `$GOPATH`. 3. cd into the `docs/book` directory 4. Run `mdbook serve` 5. Visit [http://localhost:3000](http://localhost:3000) # Steps to deploy There are no manual steps needed to deploy the website. Kubebuilder book website is deployed on Netlify. There is a preview of the website for each PR. As soon as the PR is merged, the website will be built and deployed on Netlify. ================================================ FILE: docs/book/.firebaserc ================================================ {} ================================================ FILE: docs/book/book.toml ================================================ [book] authors = ["The Kubebuilder Maintainers"] src = "src" title = "The Kubebuilder Book" [output.html] default-theme = "light" preferred-dark-theme = "navy" smart-punctuation = true additional-css = ["theme/css/markers.css", "theme/css/custom.css", "theme/css/version-dropdown.css"] git-repository-url = "https://github.com/kubernetes-sigs/kubebuilder" edit-url-template = "https://github.com/kubernetes-sigs/kubebuilder/edit/master/docs/book/{path}" sidebar-header-nav = false [preprocessor.literatego] command = "./litgo.sh" [preprocessor.markerdocs] command = "./markerdocs.sh" ================================================ FILE: docs/book/functions/handle-version.js ================================================ function notFound(info) { return { statusCode: 404, headers: {'content-type': 'text/html'}, body: ("

Not Found

"+ "

You shouldn't see this page, please file a bug

"+ `
debug details
${JSON.stringify(info)}
` ), }; } function redirectToDownload(version, file) { const loc = `https://github.com/kubernetes-sigs/kubebuilder/releases/download/v${version}/${file}`; return { statusCode: 302, headers: {'location': loc, 'content-type': 'text/plain'}, body: `Redirecting to ${loc}`, }; } exports.handler = async function(evt, ctx) { // grab the prefix too to check for coherence const [prefix, version, os, arch] = evt.path.split("/").slice(-4); if (prefix !== 'releases' || !version || !os || !arch) { return notFound({version: version, os: os, arch: arch, prefix: prefix, rawPath: evt.path}); } switch(version[0]) { case '1': // fallthrough case '2': return redirectToDownload(version, `kubebuilder_${version}_${os}_${arch}.tar.gz`); default: return redirectToDownload(version, `kubebuilder_${os}_${arch}`); } } ================================================ FILE: docs/book/install-and-build.sh ================================================ #!/bin/bash # Copyright 2020 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -e # The following code is required to allow the preview works with an upper go version # More info : https://community.netlify.com/t/go-version-1-13/5680 # Get the directory that this script file is in THIS_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) cd "$THIS_DIR" if [[ -n "$(command -v gimme)" ]]; then GO_VERSION=${GO_VERSION:-stable} # Use the provided GO_VERSION or default to 'stable' eval "$(gimme $GO_VERSION)" fi echo go version GOBIN=$THIS_DIR/functions go install ./... os=$(go env GOOS) arch=$(go env GOARCH) # translate arch to rust's conventions (if we can) if [[ ${arch} == "amd64" ]]; then arch="x86_64" elif [[ ${arch} == "arm64" ]]; then arch="aarch64" fi # translate os to rust's conventions (if we can) ext="tar.gz" cmd="tar -C /tmp -xzvf" case ${os} in windows) target="pc-windows-msvc" ext="zip" cmd="unzip -d /tmp" ;; darwin) target="apple-darwin" ;; linux) # works for linux, too target="unknown-${os}-musl" ;; *) target="unknown-${os}" ;; esac # grab mdbook MDBOOK_VERSION="v0.5.2" MDBOOK_BASENAME="mdBook-${MDBOOK_VERSION}-${arch}-${target}" MDBOOK_URL="https://github.com/rust-lang/mdBook/releases/download/${MDBOOK_VERSION}/${MDBOOK_BASENAME}.${ext}" echo "downloading ${MDBOOK_BASENAME}.${ext} from ${MDBOOK_URL}" set -x curl -fL -o /tmp/mdbook.${ext} "${MDBOOK_URL}" ${cmd} /tmp/mdbook.${ext} chmod +x /tmp/mdbook CONTROLLER_GEN_VERSION="v0.20.1" echo "grabbing the controller-gen version: ${CONTROLLER_GEN_VERSION}" go version go install sigs.k8s.io/controller-tools/cmd/controller-gen@${CONTROLLER_GEN_VERSION} # make sure we add the go bin directory to our path gobin=$(go env GOBIN) gobin=${gobin:-$(go env GOPATH)/bin} # GOBIN won't always be set :-/ export PATH=${gobin}:$PATH verb=${1:-build} /tmp/mdbook ${verb} ================================================ FILE: docs/book/litgo.sh ================================================ #!/bin/bash # Copyright 2020 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex ( pushd ./utils go build -o ../../../bin/literate-go ./litgo popd ) &>/dev/null ../../bin/literate-go "$@" ================================================ FILE: docs/book/markerdocs.sh ================================================ #!/bin/bash # Copyright 2020 The Kubernetes Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. set -ex ( pushd ./utils go build -o ../../../bin/marker-docs ./markerdocs popd ) &>/dev/null ../../bin/marker-docs "$@" ================================================ FILE: docs/book/src/SUMMARY.md ================================================ # Summary [Introduction](./introduction.md) [Architecture](./architecture.md) [Quick Start](./quick-start.md) [Getting Started](./getting-started.md) --- - [Tutorial: Building CronJob](cronjob-tutorial/cronjob-tutorial.md) - [What's in a basic project?](./cronjob-tutorial/basic-project.md) - [Every journey needs a start, every program a main](./cronjob-tutorial/empty-main.md) - [Groups and Versions and Kinds, oh my!](./cronjob-tutorial/gvks.md) - [Adding a new API](./cronjob-tutorial/new-api.md) - [Designing an API](./cronjob-tutorial/api-design.md) - [A Brief Aside: What's the rest of this stuff?](./cronjob-tutorial/other-api-files.md) - [What's in a controller?](./cronjob-tutorial/controller-overview.md) - [Implementing a controller](./cronjob-tutorial/controller-implementation.md) - [You said something about main?](./cronjob-tutorial/main-revisited.md) - [Implementing defaulting/validating webhooks](./cronjob-tutorial/webhook-implementation.md) - [Running and deploying the controller](./cronjob-tutorial/running.md) - [Deploying cert-manager](./cronjob-tutorial/cert-manager.md) - [Deploying webhooks](./cronjob-tutorial/running-webhook.md) - [Writing tests](./cronjob-tutorial/writing-tests.md) - [Tutorial: Multi-Version API](./multiversion-tutorial/tutorial.md) - [Changing things up](./multiversion-tutorial/api-changes.md) - [Hubs, spokes, and other wheel metaphors](./multiversion-tutorial/conversion-concepts.md) - [Implementing conversion](./multiversion-tutorial/conversion.md) - [and setting up the webhooks](./multiversion-tutorial/webhooks.md) - [Deployment and Testing](./multiversion-tutorial/deployment.md) --- - [Migrations](./migrations.md) - [Manual Migration Process](./migration/manual-process.md) - [Using AI](./migration/ai-helpers.md) - [Step 1: Reorganize Layout](./migration/reorganize-layout.md) - [Step 2: Discovery Commands](./migration/discovery-commands.md) - [Step 3: Port Code](./migration/port-code.md) - [Single Group to Multi-Group](./migration/multi-group.md) - [Cluster-Scoped to Namespace-Scoped](./migration/namespace-scoped.md) - [Alpha Commands](./reference/alpha_commands.md) - [alpha generate](./reference/commands/alpha_generate.md) - [alpha update](./reference/commands/alpha_update.md) --- - [Reference](./reference/reference.md) - [Generating CRDs](./reference/generating-crd.md) - [Using Finalizers](./reference/using-finalizers.md) - [Good Practices](./reference/good-practices.md) - [Raising Events](./reference/raising-events.md) - [Watching Resources](./reference/watching-resources.md) - [Owned Resources](./reference/watching-resources/secondary-owned-resources.md) - [Not Owned Resources](./reference/watching-resources/secondary-resources-not-owned.md) - [Using Predicates](./reference/watching-resources/predicates-with-watch.md) - [Kind for Dev & CI](reference/kind.md) - [What's a webhook?](reference/webhook-overview.md) - [Admission webhook](reference/admission-webhook.md) - [Webhook bootstrap problem](reference/webhook-bootstrap-problem.md) - [Markers for Config/Code Generation](./reference/markers.md) - [CRD Generation](./reference/markers/crd.md) - [CRD Validation](./reference/markers/crd-validation.md) - [CRD Processing](./reference/markers/crd-processing.md) - [Webhook](./reference/markers/webhook.md) - [Object/DeepCopy](./reference/markers/object.md) - [RBAC](./reference/markers/rbac.md) - [Scaffold](./reference/markers/scaffold.md) - [controller-gen CLI](./reference/controller-gen.md) - [completion](./reference/completion.md) - [Artifacts](./reference/artifacts.md) - [Platform Support](./reference/platform.md) - [Monitoring with Pprof](./reference/pprof-tutorial.md) - [Manager and CRDs Scope](./reference/scopes.md) - [Manager Scope](./reference/manager-scope.md) - [CRD Scope](./reference/crd-scope.md) - [Sub-Module Layouts](./reference/submodule-layouts.md) - [Using an external Resource / API](./reference/using_an_external_resource.md) - [Configuring EnvTest](./reference/envtest.md) - [Metrics](./reference/metrics.md) - [Reference](./reference/metrics-reference.md) - [Project config](./reference/project-config.md) - [Versions Compatibility and Supportability](./versions_compatibility_supportability.md) --- - [Plugins][plugins] - [Available Plugins](./plugins/available-plugins.md) - [autoupdate/v1-alpha](./plugins/available/autoupdate-v1-alpha.md) - [deploy-image/v1-alpha](./plugins/available/deploy-image-plugin-v1-alpha.md) - [go/v4](./plugins/available/go-v4-plugin.md) - [grafana/v1-alpha](./plugins/available/grafana-v1-alpha.md) - [helm/v1-alpha](./plugins/available/helm-v1-alpha.md) - [helm/v2-alpha](./plugins/available/helm-v2-alpha.md) - [kustomize/v2](./plugins/available/kustomize-v2.md) - [Extending](./plugins/extending.md) - [CLI and Plugins](./plugins/extending/extending_cli_features_and_plugins.md) - [External Plugins](./plugins/extending/external-plugins.md) - [Custom Markers](./plugins/extending/custom-markers.md) - [E2E Tests](./plugins/extending/testing-plugins.md) - [Plugins Versioning](./plugins/plugins-versioning.md) --- [FAQ](./faq.md) [plugins]: ./plugins/plugins.md ================================================ FILE: docs/book/src/TODO.md ================================================ # Page Not Found The page you are looking for could not be found. This might be because: 1. The page has been moved or renamed 2. The page is no longer available 3. The URL was entered incorrectly Please try: - Going back to the [home page](https://book.kubebuilder.io/) - Using the search function - Suggest an edit [documentation index](https://github.com/kubernetes-sigs/kubebuilder/tree/master/docs/book/src) Check out if someone is working on your issue [report an issue](https://github.com/kubernetes-sigs/kubebuilder/issues) If you believe this is an error, please [report an issue](https://github.com/kubernetes-sigs/kubebuilder/issues/new?template=BLANK_ISSUE) Reach out to us on [Slack](https://kubernetes.slack.com/messages/kubebuilder) ================================================ FILE: docs/book/src/architecture.md ================================================ # Architecture Concept Diagram The following diagram will help you get a better idea over the Kubebuilder concepts and architecture. {{#include ./kb_concept_diagram.svg}} ================================================ FILE: docs/book/src/cronjob-tutorial/api-design.md ================================================ # Designing an API In Kubernetes, we have a few rules for how we design APIs. Namely, all serialized fields *must* be `camelCase`, so we use JSON struct tags to specify this. We can also use the `omitempty` struct tag to mark that a field should be omitted from serialization when empty. Fields may use most of the primitive types. Numbers are the exception: for API compatibility purposes, we accept three forms of numbers: `int32` and `int64` for integers, and `resource.Quantity` for decimals.
Hold up, what's a Quantity? Quantities are a special notation for decimal numbers that have an explicitly fixed representation that makes them more portable across machines. You've probably noticed them when specifying resources requests and limits on pods in Kubernetes. They conceptually work similar to floating point numbers: they have a significant, base, and exponent. Their serializable and human readable format uses whole numbers and suffixes to specify values much the way we describe computer storage. For instance, the value `2m` means `0.002` in decimal notation. `2Ki` means `2048` in decimal, while `2K` means `2000` in decimal. If we want to specify fractions, we switch to a suffix that lets us use a whole number: `2.5` is `2500m`. There are two supported bases: 10 and 2 (called decimal and binary, respectively). Decimal base is indicated with "normal" SI suffixes (e.g. `M` and `K`), while Binary base is specified in "mebi" notation (e.g. `Mi` and `Ki`). Think [megabytes vs mebibytes](https://en.wikipedia.org/wiki/Binary_prefix).
There's one other special type that we use: `metav1.Time`. This functions identically to `time.Time`, except that it has a fixed, portable serialization format. With that out of the way, let's take a look at what our CronJob object looks like! {{#literatego ./testdata/project/api/v1/cronjob_types.go}} Now that we have an API, we'll need to write a controller to actually implement the functionality. ================================================ FILE: docs/book/src/cronjob-tutorial/basic-project.md ================================================ # What's in a basic project? When scaffolding out a new project, Kubebuilder provides us with a few basic pieces of boilerplate. ## Build Infrastructure First up, basic infrastructure for building your project:
go.mod: A new Go module matching our project, with basic dependencies ```go {{#include ./testdata/project/go.mod}} ```
Makefile: Make targets for building and deploying your controller ```makefile {{#include ./testdata/project/Makefile}} ```
PROJECT: Kubebuilder metadata for scaffolding new components ```yaml {{#include ./testdata/project/PROJECT}} ```
## Launch Configuration We also get launch configurations under the [`config/`](https://github.com/kubernetes-sigs/kubebuilder/tree/master/docs/book/src/cronjob-tutorial/testdata/project/config) directory. Right now, it just contains [Kustomize](https://sigs.k8s.io/kustomize) YAML definitions required to launch our controller on a cluster, but once we get started writing our controller, it'll also hold our CustomResourceDefinitions, RBAC configuration, and WebhookConfigurations. [`config/default`](https://github.com/kubernetes-sigs/kubebuilder/tree/master/docs/book/src/cronjob-tutorial/testdata/project/config/default) contains a [Kustomize base](https://github.com/kubernetes-sigs/kubebuilder/blob/master/docs/book/src/cronjob-tutorial/testdata/project/config/default/kustomization.yaml) for launching the controller in a standard configuration. Each other directory contains a different piece of configuration, refactored out into its own base: - [`config/manager`](https://github.com/kubernetes-sigs/kubebuilder/tree/master/docs/book/src/cronjob-tutorial/testdata/project/config/manager): launch your controllers as pods in the cluster - [`config/rbac`](https://github.com/kubernetes-sigs/kubebuilder/tree/master/docs/book/src/cronjob-tutorial/testdata/project/config/rbac): permissions required to run your controllers under their own service account ## The Entrypoint Last, but certainly not least, Kubebuilder scaffolds out the basic entrypoint of our project: `main.go`. Let's take a look at that next... ================================================ FILE: docs/book/src/cronjob-tutorial/cert-manager.md ================================================ # Deploying cert-manager We suggest using [cert-manager](https://github.com/cert-manager/cert-manager) for provisioning the certificates for the webhook server. Other solutions should also work as long as they put the certificates in the desired location. You can follow [the cert-manager documentation](https://cert-manager.io/docs/installation/) to install it. cert-manager also has a component called [CA Injector](https://cert-manager.io/docs/concepts/ca-injector/), which is responsible for injecting the CA bundle into the [`MutatingWebhookConfiguration`](https://pkg.go.dev/k8s.io/api/admissionregistration/v1#MutatingWebhookConfiguration) / [`ValidatingWebhookConfiguration`](https://pkg.go.dev/k8s.io/api/admissionregistration/v1#ValidatingWebhookConfiguration). To accomplish that, you need to use an annotation with key `cert-manager.io/inject-ca-from` in the [`MutatingWebhookConfiguration`](https://pkg.go.dev/k8s.io/api/admissionregistration/v1#MutatingWebhookConfiguration) / [`ValidatingWebhookConfiguration`](https://pkg.go.dev/k8s.io/api/admissionregistration/v1#ValidatingWebhookConfiguration) objects. The value of the annotation should point to an existing [certificate request instance](https://cert-manager.io/docs/concepts/certificaterequest/) in the format of `/`. This is the [kustomize](https://github.com/kubernetes-sigs/kustomize) patch we used for annotating the [`MutatingWebhookConfiguration`](https://pkg.go.dev/k8s.io/api/admissionregistration/v1#MutatingWebhookConfiguration) / [`ValidatingWebhookConfiguration`](https://pkg.go.dev/k8s.io/api/admissionregistration/v1#ValidatingWebhookConfiguration) objects. ================================================ FILE: docs/book/src/cronjob-tutorial/controller-implementation.md ================================================ # Implementing a controller The basic logic of our CronJob controller is this: 1. Load the named CronJob 2. List all active jobs, and update the status 3. Clean up old jobs according to the history limits 4. Check if we're suspended (and don't do anything else if we are) 5. Get the next scheduled run 6. Run a new job if it's on schedule, not past the deadline, and not blocked by our concurrency policy 7. Requeue when we either see a running job (done automatically) or it's time for the next scheduled run. {{#literatego ./testdata/project/internal/controller/cronjob_controller.go}} That was a doozy, but now we've got a working controller. Let's test against the cluster, then, if we don't have any issues, deploy it! ================================================ FILE: docs/book/src/cronjob-tutorial/controller-overview.md ================================================ # What's in a controller? Controllers are the core of Kubernetes, and of any operator. It's a controller's job to ensure that, for any given object, the actual state of the world (both the cluster state, and potentially external state like running containers for Kubelet or loadbalancers for a cloud provider) matches the desired state in the object. Each controller focuses on one *root* Kind, but may interact with other Kinds. We call this process *reconciling*. In controller-runtime, the logic that implements the reconciling for a specific kind is called a [*Reconciler*](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile?tab=doc). A reconciler takes the name of an object, and returns whether or not we need to try again (e.g. in case of errors or periodic controllers, like the HorizontalPodAutoscaler). {{#literatego ./testdata/emptycontroller.go}} Now that we've seen the basic structure of a reconciler, let's fill out the logic for `CronJob`s. ================================================ FILE: docs/book/src/cronjob-tutorial/cronjob-tutorial.md ================================================ # Tutorial: Building CronJob Too many tutorials start out with some really contrived setup, or some toy application that gets the basics across, and then stalls out on the more complicated stuff. Instead, this tutorial should take you through (almost) the full gamut of complexity with Kubebuilder, starting off simple and building up to something pretty full-featured. Let's pretend (and sure, this is a teensy bit contrived) that we've finally gotten tired of the maintenance burden of the non-Kubebuilder implementation of the CronJob controller in Kubernetes, and we'd like to rewrite it using Kubebuilder. The job (no pun intended) of the *CronJob* controller is to run one-off tasks on the Kubernetes cluster at regular intervals. It does this by building on top of the *Job* controller, whose task is to run one-off tasks once, seeing them to completion. Instead of trying to tackle rewriting the Job controller as well, we'll use this as an opportunity to see how to interact with external types. ## Scaffolding Out Our Project As covered in the [quick start](../quick-start.md), we'll need to scaffold out a new project. Make sure you've [installed Kubebuilder](../quick-start.md#installation), then scaffold out a new project: ```bash # create a project directory, and then run the init command. mkdir project cd project # we'll use a domain of tutorial.kubebuilder.io, # so all API groups will be .tutorial.kubebuilder.io. kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/project ``` Now that we've got a project in place, let's take a look at what Kubebuilder has scaffolded for us so far... [GOPATH-golang-docs]: https://golang.org/doc/code.html#GOPATH [go-modules-blogpost]: https://blog.golang.org/using-go-modules ================================================ FILE: docs/book/src/cronjob-tutorial/empty-main.md ================================================ # Every journey needs a start, every program needs a main {{#literatego ./testdata/emptymain.go}} With that out of the way, we can get on to scaffolding our API! ================================================ FILE: docs/book/src/cronjob-tutorial/gvks.md ================================================ # Groups and Versions and Kinds, oh my! Before we get started with our API, we should talk about terminology a bit. When we talk about APIs in Kubernetes, we often use 4 terms: *groups*, *versions*, *kinds*, and *resources*. ## Groups and Versions An *API Group* in Kubernetes is simply a collection of related functionality. Each group has one or more *versions*, which, as the name suggests, allow us to change how an API works over time. ## Kinds and Resources Each API group-version contains one or more API types, which we call *Kinds*. While a Kind may change forms between versions, each form must be able to store all the data of the other forms, somehow (we can store the data in fields, or in annotations). This means that using an older API version won't cause newer data to be lost or corrupted. See the [Kubernetes API guidelines](https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md) for more information. You'll also hear mention of *resources* on occasion. A resource is simply a use of a Kind in the API. Often, there's a one-to-one mapping between Kinds and resources. For instance, the `pods` resource corresponds to the `Pod` Kind. However, sometimes, the same Kind may be returned by multiple resources. For instance, the `Scale` Kind is returned by all scale subresources, like `deployments/scale` or `replicasets/scale`. This is what allows the Kubernetes HorizontalPodAutoscaler to interact with different resources. With CRDs, however, each Kind will correspond to a single resource. Notice that resources are always lowercase, and by convention are the lowercase form of the Kind. ## So, how does that correspond to Go? When we refer to a kind in a particular group version, we'll call it a *GroupVersionKind*, or GVK for short. Same with resources and GVR. As we'll see shortly, each GVK corresponds to a given root Go type in a package. Now that we have our terminology straight, we can *actually* create our API! ## So, how can we create our API? In the next section, [Adding a new API](../cronjob-tutorial/new-api.html), we will check how the tool helps us to create our own APIs with the command `kubebuilder create api`. The goal of this command is to create a Custom Resource (CR) and Custom Resource Definition (CRD) for our Kind(s). To check it further see; [Extend the Kubernetes API with CustomResourceDefinitions][kubernetes-extend-api]. ## But, why create APIs at all? New APIs are how we teach Kubernetes about our custom objects. The Go structs are used to generate a CRD which includes the schema for our data as well as tracking data like what our new type is called. We can then create instances of our custom objects which will be managed by our [controllers][controllers]. Our APIs and resources represent our solutions on the clusters. Basically, the CRDs are a definition of our customized Objects, and the CRs are an instance of it. ## Ah, do you have an example? Let’s think about the classic scenario where the goal is to have an application and its database running on the platform with Kubernetes. Then, one CRD could represent the App, and another one could represent the DB. By having one CRD to describe the App and another one for the DB, we will not be hurting concepts such as encapsulation, the single responsibility principle, and cohesion. Damaging these concepts could cause unexpected side effects, such as difficulty in extending, reuse, or maintenance, just to mention a few. In this way, we can create the App CRD which will have its controller and which would be responsible for things like creating Deployments that contain the App and creating Services to access it and etc. Similarly, we could create a CRD to represent the DB, and deploy a controller that would manage DB instances. ## Err, but what's that Scheme thing? The `Scheme` we saw before is simply a way to keep track of what Go type corresponds to a given GVK (don't be overwhelmed by its [godocs](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime?tab=doc#Scheme)). For instance, suppose we mark the `"tutorial.kubebuilder.io/api/v1".CronJob{}` type as being in the `batch.tutorial.kubebuilder.io/v1` API group (implicitly saying it has the Kind `CronJob`). Then, we can later construct a new `&CronJob{}` given some JSON from the API server that says ```json { "kind": "CronJob", "apiVersion": "batch.tutorial.kubebuilder.io/v1", ... } ``` or properly look up the group version when we go to submit a `&CronJob{}` in an update. [kubernetes-extend-api]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ [controllers]: ../cronjob-tutorial/controller-overview.md ================================================ FILE: docs/book/src/cronjob-tutorial/main-revisited.md ================================================ # You said something about main? But first, remember how we said we'd [come back to `main.go` again](/cronjob-tutorial/empty-main.md)? Let's take a look and see what's changed, and what we need to add. {{#literatego ./testdata/project/cmd/main.go}} *Now* we can implement our controller. ================================================ FILE: docs/book/src/cronjob-tutorial/new-api.md ================================================ # Adding a new API To scaffold out a new Kind (you were paying attention to the [last chapter](./gvks.md#kinds-and-resources), right?) and corresponding controller, we can use `kubebuilder create api`: ```bash kubebuilder create api --group batch --version v1 --kind CronJob ``` Press `y` for "Create Resource" and "Create Controller". The first time we call this command for each group-version, it will create a directory for the new group-version. In this case, the [`api/v1/`](https://sigs.k8s.io/kubebuilder/docs/book/src/cronjob-tutorial/testdata/project/api/v1) directory is created, corresponding to the `batch.tutorial.kubebuilder.io/v1` (remember our [`--domain` setting](cronjob-tutorial.md#scaffolding-out-our-project) from the beginning?). It has also added a file for our `CronJob` Kind, `api/v1/cronjob_types.go`. Each time we call the command with a different kind, it'll add a corresponding new file. Let's take a look at what we've been given out of the box, then we can move on to filling it out. {{#literatego ./testdata/emptyapi.go}} Now that we've seen the basic structure, let's fill it out! ================================================ FILE: docs/book/src/cronjob-tutorial/other-api-files.md ================================================ # A Brief Aside: What's the rest of this stuff? If you've taken a peek at the rest of the files in the [`api/v1/`](https://sigs.k8s.io/kubebuilder/docs/book/src/cronjob-tutorial/testdata/project/api/v1) directory, you might have noticed two additional files beyond `cronjob_types.go`: `groupversion_info.go` and `zz_generated.deepcopy.go`. Neither of these files ever needs to be edited (the former stays the same and the latter is autogenerated), but it's useful to know what's in them. ## `groupversion_info.go` `groupversion_info.go` contains common metadata about the group-version: {{#literatego ./testdata/project/api/v1/groupversion_info.go}} ## `zz_generated.deepcopy.go` `zz_generated.deepcopy.go` contains the autogenerated implementation of the aforementioned `runtime.Object` interface, which marks all of our root types as representing Kinds. The core of the `runtime.Object` interface is a deep-copy method, `DeepCopyObject`. The `object` generator in controller-tools also generates two other handy methods for each root type and all its sub-types: `DeepCopy` and `DeepCopyInto`. ================================================ FILE: docs/book/src/cronjob-tutorial/running-webhook.md ================================================ # Deploying Admission Webhooks ## cert-manager You need to follow [this](./cert-manager.md) to install the cert-manager bundle. ## Build your image Run the following command to build your image locally. ```bash make docker-build docker-push IMG=/:tag ``` ## Deploy Webhooks You need to enable the webhook and cert manager configuration through kustomize. `config/default/kustomization.yaml` should have the following webhook-related sections uncommented: **Resources** - Add the webhook and cert-manager resources: ```yaml {{#include ./testdata/project/config/default/kustomization.yaml:webhook-resources}} ``` **Patches** - Add the webhook manager patch: ```yaml {{#include ./testdata/project/config/default/kustomization.yaml:webhook-patch}} ``` **Replacements** - Add the webhook certificate replacements: ```yaml {{#include ./testdata/project/config/default/kustomization.yaml:webhook-replacements}} ``` And `config/crd/kustomization.yaml` should now look like the following: ```yaml {{#include ./testdata/project/config/crd/kustomization.yaml}} ``` Now you can deploy it to your cluster by ```bash make deploy IMG=/:tag ``` Wait a while till the webhook pod comes up and the certificates are provisioned. It usually completes within 1 minute. Now you can create a valid CronJob to test your webhooks. The creation should successfully go through. ```bash kubectl create -f config/samples/batch_v1_cronjob.yaml ``` You can also try to create an invalid CronJob (e.g. use an ill-formatted schedule field). You should see a creation failure with a validation error. [namespaceSelector]: https://github.com/kubernetes/api/blob/kubernetes-1.14.5/admissionregistration/v1beta1/types.go#L189-L233 [objectSelector]: https://github.com/kubernetes/api/blob/kubernetes-1.15.2/admissionregistration/v1beta1/types.go#L262-L274 ================================================ FILE: docs/book/src/cronjob-tutorial/running.md ================================================ # Running and deploying the controller ### Optional If opting to make any changes to the API definitions, then before proceeding, generate the manifests like CRs or CRDs with ```bash make manifests ``` To test out the controller, we can run it locally against the cluster. Before we do so, though, we'll need to install our CRDs, as per the [quick start](/quick-start.md). This will automatically update the YAML manifests using controller-tools, if needed: ```bash make install ``` Now that we've installed our CRDs, we can run the controller against our cluster. This will use whatever credentials that we connect to the cluster with, so we don't need to worry about RBAC just yet. In a separate terminal, run ```bash export ENABLE_WEBHOOKS=false make run ``` You should see logs from the controller about starting up, but it won't do anything just yet. At this point, we need a CronJob to test with. Let's write a sample to `config/samples/batch_v1_cronjob.yaml`, and use that: ```yaml {{#include ./testdata/project/config/samples/batch_v1_cronjob.yaml}} ``` ```bash kubectl create -f config/samples/batch_v1_cronjob.yaml ``` At this point, you should see a flurry of activity. If you watch the changes, you should see your cronjob running, and updating status: ```bash kubectl get cronjob.batch.tutorial.kubebuilder.io -o yaml kubectl get job ``` Now that we know it's working, we can run it in the cluster. Stop the `make run` invocation, and run ```bash make docker-build docker-push IMG=/:tag make deploy IMG=/:tag ``` If we list cronjobs again like we did before, we should see the controller functioning again! [pre-rbc-gke]: https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control#iam-rolebinding-bootstrap ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/emptyapi.go ================================================ /* Copyright 2022. 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* We start out simply enough: we import the `meta/v1` API group, which is not normally exposed by itself, but instead contains metadata common to all Kubernetes Kinds. */ package v1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) /* Next, we define types for the Spec and Status of our Kind. Kubernetes functions by reconciling desired state (`Spec`) with actual cluster state (other objects' `Status`) and external state, and then recording what it observed (`Status`). Thus, every *functional* object includes spec and status. A few types, like `ConfigMap` don't follow this pattern, since they don't encode desired state, but most types do. */ // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // CronJobSpec defines the desired state of CronJob type CronJobSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file } // CronJobStatus defines the observed state of CronJob type CronJobStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file } /* Next, we define the types corresponding to actual Kinds, `CronJob` and `CronJobList`. `CronJob` is our root type, and describes the `CronJob` kind. Like all Kubernetes objects, it contains `TypeMeta` (which describes API version and Kind), and also contains `ObjectMeta`, which holds things like name, namespace, and labels. `CronJobList` is simply a container for multiple `CronJob`s. It's the Kind used in bulk operations, like LIST. In general, we never modify either of these -- all modifications go in either Spec or Status. That little `+kubebuilder:object:root` comment is called a marker. We'll see more of them in a bit, but know that they act as extra metadata, telling [controller-tools](https://github.com/kubernetes-sigs/controller-tools) (our code and YAML generator) extra information. This particular one tells the `object` generator that this type represents a Kind. Then, the `object` generator generates an implementation of the [runtime.Object](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime?tab=doc#Object) interface for us, which is the standard interface that all types representing Kinds must implement. */ // +kubebuilder:object:root=true // +kubebuilder:subresource:status // CronJob is the Schema for the cronjobs API type CronJob struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec CronJobSpec `json:"spec,omitempty"` Status CronJobStatus `json:"status,omitempty"` } // +kubebuilder:object:root=true // CronJobList contains a list of CronJob type CronJobList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []CronJob `json:"items"` } /* Finally, we add the Go types to the API group. This allows us to add the types in this API group to any [Scheme](https://pkg.go.dev/k8s.io/apimachinery/pkg/runtime?tab=doc#Scheme). */ func init() { SchemeBuilder.Register(&CronJob{}, &CronJobList{}) } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/emptycontroller.go ================================================ /* Copyright 2022. 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* First, we start out with some standard imports. As before, we need the core controller-runtime library, as well as the client package, and the package for our API types. */ package controllers import ( "context" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) /* Next, kubebuilder has scaffolded a basic reconciler struct for us. Pretty much every reconciler needs to log, and needs to be able to fetch objects, so these are added out of the box. */ // CronJobReconciler reconciles a CronJob object type CronJobReconciler struct { client.Client Scheme *runtime.Scheme } /* Most controllers eventually end up running on the cluster, so they need RBAC permissions, which we specify using controller-tools [RBAC markers](/reference/markers/rbac.md). These are the bare minimum permissions needed to run. As we add more functionality, we'll need to revisit these. */ // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch /* The `ClusterRole` manifest at `config/rbac/role.yaml` is generated from the above markers via controller-gen with the following command: */ // make manifests /* NOTE: If you receive an error, please run the specified command in the error and re-run `make manifests`. */ /* `Reconcile` actually performs the reconciling for a single named object. Our [Request](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/reconcile?tab=doc#Request) just has a name, but we can use the client to fetch that object from the cache. We return an empty result and no error, which indicates to controller-runtime that we've successfully reconciled this object and don't need to try again until there's some changes. Most controllers need a logging handle and a context, so we set them up here. The [context](https://golang.org/pkg/context/) is used to allow cancellation of requests, and potentially things like tracing. It's the first argument to all client methods. The `Background` context is just a basic context without any extra data or timing restrictions. The logging handle lets us log. controller-runtime uses structured logging through a library called [logr](https://github.com/go-logr/logr). As we'll see shortly, logging works by attaching key-value pairs to a static message. We can pre-assign some pairs at the top of our reconcile method to have those attached to all log lines in this reconciler. */ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { _ = logf.FromContext(ctx) // your logic here return ctrl.Result{}, nil } /* Finally, we add this reconciler to the manager, so that it gets started when the manager is started. For now, we just note that this reconciler operates on `CronJob`s. Later, we'll use this to mark that we care about related objects as well. */ func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&batchv1.CronJob{}). Complete(r) } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/emptymain.go ================================================ /* Copyright 2022 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* Our package starts out with some basic imports. Particularly: - The core [controller-runtime](https://pkg.go.dev/sigs.k8s.io/controller-runtime?tab=doc) library - The default controller-runtime logging, [Zap](https://pkg.go.dev/go.uber.org/zap) (more on that a bit later) */ package main import ( "flag" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/cache" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" // +kubebuilder:scaffold:imports ) /* Every set of controllers needs a [*Scheme*](https://book.kubebuilder.io/cronjob-tutorial/gvks.html#err-but-whats-that-scheme-thing), which provides mappings between Kinds and their corresponding Go types. We'll talk a bit more about Kinds when we write our API definition, so just keep this in mind for later. */ var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } /* At this point, our main function is fairly simple: - We set up some basic flags for metrics. - We instantiate a [*manager*](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/manager?tab=doc#Manager), which keeps track of running all of our controllers, as well as setting up shared caches and clients to the API server (notice we tell the manager about our Scheme). - We run our manager, which in turn runs all of our controllers and webhooks. The manager is set up to run until it receives a graceful shutdown signal. This way, when we're running on Kubernetes, we behave nicely with graceful pod termination. While we don't have anything to run just yet, remember where that `+kubebuilder:scaffold:builder` comment is -- things'll get interesting there soon. */ func main() { var metricsAddr string var enableLeaderElection bool var probeAddr string flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: server.Options{ BindAddress: metricsAddr, }, WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}), HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "80807133.tutorial.kubebuilder.io", }) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) } /* Note that the `Manager` can restrict the namespace that all controllers will watch for resources by: */ mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Cache: cache.Options{ DefaultNamespaces: map[string]cache.Config{ namespace: {}, }, }, Metrics: server.Options{ BindAddress: metricsAddr, }, WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}), HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "80807133.tutorial.kubebuilder.io", }) /* The above example will change the scope of your project to a single `Namespace`. In this scenario, it is also suggested to restrict the provided authorization to this namespace by replacing the default `ClusterRole` and `ClusterRoleBinding` to `Role` and `RoleBinding` respectively. For further information see the Kubernetes documentation about [Using RBAC Authorization](https://kubernetes.io/docs/reference/access-authn-authz/rbac/). Also, it is possible to use the [`DefaultNamespaces`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/cache#Options) from `cache.Options{}` to cache objects in a specific set of namespaces: */ var namespaces []string // List of Namespaces defaultNamespaces := make(map[string]cache.Config) for _, ns := range namespaces { defaultNamespaces[ns] = cache.Config{} } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Cache: cache.Options{ DefaultNamespaces: defaultNamespaces, }, Metrics: server.Options{ BindAddress: metricsAddr, }, WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}), HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "80807133.tutorial.kubebuilder.io", }) /* For further information see [`cache.Options{}`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/cache#Options) */ // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up ready check") os.Exit(1) } setupLog.Info("starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1) } } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/finalizer_example.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // +kubebuilder:docs-gen:collapse=Apache License /* First, we start out with some standard imports. As before, we need the core controller-runtime library, as well as the client package, and the package for our API types. */ package controllers import ( "context" "k8s.io/kubernetes/pkg/apis/batch" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Imports /* By default, kubebuilder will include the RBAC rules necessary to update finalizers for CronJobs. */ // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update /* The code snippet below shows skeleton code for implementing a finalizer. */ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := r.Log.WithValues("cronjob", req.NamespacedName) cronJob := &batchv1.CronJob{} if err := r.Get(ctx, req.NamespacedName, cronJob); err != nil { log.Error(err, "unable to fetch CronJob") // we'll ignore not-found errors, since they can't be fixed by an immediate // requeue (we'll need to wait for a new notification), and we can get them // on deleted requests. return ctrl.Result{}, client.IgnoreNotFound(err) } // name of our custom finalizer myFinalizerName := "batch.tutorial.kubebuilder.io/finalizer" // examine DeletionTimestamp to determine if object is under deletion if cronJob.ObjectMeta.DeletionTimestamp.IsZero() { // The object is not being deleted, so if it does not have our finalizer, // then let's add the finalizer and update the object. This is equivalent // to registering our finalizer. if !controllerutil.ContainsFinalizer(cronJob, myFinalizerName) { controllerutil.AddFinalizer(cronJob, myFinalizerName) if err := r.Update(ctx, cronJob); err != nil { return ctrl.Result{}, err } } } else { // The object is being deleted if controllerutil.ContainsFinalizer(cronJob, myFinalizerName) { // our finalizer is present, so let's handle any external dependency if err := r.deleteExternalResources(cronJob); err != nil { // if fail to delete the external dependency here, return with error // so that it can be retried. return ctrl.Result{}, err } // remove our finalizer from the list and update it. controllerutil.RemoveFinalizer(cronJob, myFinalizerName) if err := r.Update(ctx, cronJob); err != nil { return ctrl.Result{}, err } } // Stop reconciliation as the item is being deleted return ctrl.Result{}, nil } // Your reconcile logic return ctrl.Result{}, nil } func (r *Reconciler) deleteExternalResources(cronJob *batch.CronJob) error { // // delete any external resources associated with the cronJob // // Ensure that delete implementation is idempotent and safe to invoke // multiple times for same object. } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.custom-gcl.yml ================================================ # This file configures golangci-lint with module plugins. # When you run 'make lint', it will automatically build a custom golangci-lint binary # with all the plugins listed below. # # See: https://golangci-lint.run/plugins/module-plugins/ version: v2.8.0 plugins: # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs) - module: "sigs.k8s.io/logtools" import: "sigs.k8s.io/logtools/logcheck/gclplugin" version: latest ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.devcontainer/devcontainer.json ================================================ { "name": "Kubebuilder DevContainer", "image": "golang:1.25", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": { "moby": false, "dockerDefaultAddressPool": "base=172.30.0.0/16,size=24" }, "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/common-utils:2": { "upgradePackages": true } }, "runArgs": ["--privileged", "--init"], "customizations": { "vscode": { "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, "extensions": [ "ms-kubernetes-tools.vscode-kubernetes-tools", "ms-azuretools.vscode-docker" ] } }, "remoteEnv": { "GO111MODULE": "on" }, "onCreateCommand": "bash .devcontainer/post-install.sh" } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.devcontainer/post-install.sh ================================================ #!/bin/bash set -euo pipefail echo "====================================" echo "Kubebuilder DevContainer Setup" echo "====================================" # Verify running as root (required for installing to /usr/local/bin and /etc) if [ "$(id -u)" -ne 0 ]; then echo "ERROR: This script must be run as root" exit 1 fi echo "" echo "Detecting system architecture..." # Detect architecture using uname MACHINE=$(uname -m) case "${MACHINE}" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; *) echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64" ARCH="amd64" ;; esac echo "Architecture: ${ARCH}" echo "" echo "------------------------------------" echo "Setting up bash completion..." echo "------------------------------------" BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions" # Enable bash-completion in root's .bashrc (devcontainer runs as root) if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc echo "Added bash-completion to .bashrc" fi echo "" echo "------------------------------------" echo "Installing development tools..." echo "------------------------------------" # Install kind if ! command -v kind &> /dev/null; then echo "Installing kind..." curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}" chmod +x /usr/local/bin/kind echo "kind installed successfully" fi # Generate kind bash completion if command -v kind &> /dev/null; then if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then echo "kind completion installed" else echo "WARNING: Failed to generate kind completion" fi fi # Install kubebuilder if ! command -v kubebuilder &> /dev/null; then echo "Installing kubebuilder..." curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}" chmod +x /usr/local/bin/kubebuilder echo "kubebuilder installed successfully" fi # Generate kubebuilder bash completion if command -v kubebuilder &> /dev/null; then if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then echo "kubebuilder completion installed" else echo "WARNING: Failed to generate kubebuilder completion" fi fi # Install kubectl if ! command -v kubectl &> /dev/null; then echo "Installing kubectl..." KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt) curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" chmod +x /usr/local/bin/kubectl echo "kubectl installed successfully" fi # Generate kubectl bash completion if command -v kubectl &> /dev/null; then if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then echo "kubectl completion installed" else echo "WARNING: Failed to generate kubectl completion" fi fi # Generate Docker bash completion if command -v docker &> /dev/null; then if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then echo "docker completion installed" else echo "WARNING: Failed to generate docker completion" fi fi echo "" echo "------------------------------------" echo "Configuring Docker environment..." echo "------------------------------------" # Wait for Docker to be ready echo "Waiting for Docker to be ready..." for i in {1..30}; do if docker info >/dev/null 2>&1; then echo "Docker is ready" break fi if [ "$i" -eq 30 ]; then echo "WARNING: Docker not ready after 30s" fi sleep 1 done # Create kind network (ignore if already exists) if ! docker network inspect kind >/dev/null 2>&1; then if docker network create kind >/dev/null 2>&1; then echo "Created kind network" else echo "WARNING: Failed to create kind network (may already exist)" fi fi echo "" echo "------------------------------------" echo "Verifying installations..." echo "------------------------------------" kind version kubebuilder version kubectl version --client docker --version go version echo "" echo "====================================" echo "DevContainer ready!" echo "====================================" echo "All development tools installed successfully." echo "You can now start building Kubernetes operators." ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.dockerignore ================================================ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file # Ignore everything by default and re-include only needed files ** # Re-include Go source files (but not *_test.go) !**/*.go **/*_test.go # Re-include Go module files !go.mod !go.sum ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.github/workflows/lint.yml ================================================ name: Lint on: push: pull_request: jobs: lint: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Check linter configuration run: make lint-config - name: Run linter run: make lint ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.github/workflows/test-chart.yml ================================================ name: Test Chart on: push: pull_request: jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Prepare project run: | go mod tidy make docker-build IMG=controller:latest kind load docker-image controller:latest - name: Install Helm run: make install-helm - name: Lint Helm Chart run: | helm lint ./dist/chart - name: Install cert-manager via Helm (wait for readiness) run: | helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true \ --wait \ --timeout 300s # TODO: Uncomment if Prometheus is enabled # - name: Install Prometheus Operator CRDs # run: | # helm repo add prometheus-community https://prometheus-community.github.io/helm-charts # helm repo update # helm install prometheus-crds prometheus-community/prometheus-operator-crds - name: Deploy manager via Helm run: | make helm-deploy IMG=project:v0.1.0 - name: Check Helm release status run: | make helm-status ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.github/workflows/test-e2e.yml ================================================ name: E2E Tests on: push: pull_request: jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Running Test e2e run: | go mod tidy make test-e2e ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.github/workflows/test.yml ================================================ name: Tests on: push: pull_request: jobs: test: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Running Tests run: | go mod tidy make test ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin/* Dockerfile.cross # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Go workspace file go.work # Kubernetes Generated files - skip generated files, except for vendored files !vendor/**/zz_generated.* # editor and IDE paraphernalia .idea .vscode *.swp *.swo *~ # Kubeconfig might contain secrets *.kubeconfig ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/.golangci.yml ================================================ version: "2" run: allow-parallel-runners: true linters: default: none enable: - copyloopvar - dupl - errcheck - ginkgolinter - goconst - gocyclo - govet - ineffassign - lll - modernize - misspell - nakedret - prealloc - revive - staticcheck - unconvert - unparam - unused - logcheck settings: custom: logcheck: type: "module" description: Checks Go logging calls for Kubernetes logging conventions. revive: rules: - name: comment-spacings - name: import-shadowing modernize: disable: - omitzero exclusions: generated: lax rules: - linters: - lll path: api/* - linters: - dupl - lll path: internal/* paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/AGENTS.md ================================================ # project - AI Agent Guide ## Project Structure **Single-group layout (default):** ``` cmd/main.go Manager entry (registers controllers/webhooks) api//*_types.go CRD schemas (+kubebuilder markers) api//zz_generated.* Auto-generated (DO NOT EDIT) internal/controller/* Reconciliation logic internal/webhook/* Validation/defaulting (if present) config/crd/bases/* Generated CRDs (DO NOT EDIT) config/rbac/role.yaml Generated RBAC (DO NOT EDIT) config/samples/* Example CRs (edit these) Makefile Build/test/deploy commands PROJECT Kubebuilder metadata Auto-generated (DO NOT EDIT) ``` **Multi-group layout** (for projects with multiple API groups): ``` api///*_types.go CRD schemas by group internal/controller//* Controllers by group internal/webhook///* Webhooks by group and version (if present) ``` Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`. **To convert to multi-group layout:** 1. Run: `kubebuilder edit --multigroup=true` 2. Move APIs: `mkdir -p api/ && mv api/ api//` 3. Move controllers: `mkdir -p internal/controller/ && mv internal/controller/*.go internal/controller//` 4. Move webhooks (if present): `mkdir -p internal/webhook/ && mv internal/webhook/ internal/webhook//` 5. Update import paths in all files 6. Fix `path` in `PROJECT` file for each resource 7. Update test suite CRD paths (add one more `..` to relative paths) ## Critical Rules ### Never Edit These (Auto-Generated) - `config/crd/bases/*.yaml` - from `make manifests` - `config/rbac/role.yaml` - from `make manifests` - `config/webhook/manifests.yaml` - from `make manifests` - `**/zz_generated.*.go` - from `make generate` - `PROJECT` - from `kubebuilder [OPTIONS]` ### Never Remove Scaffold Markers Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers. ### Keep Project Structure Do not move files around. The CLI expects files in specific locations. ### Always Use CLI Commands Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually. ### E2E Tests Require an Isolated Kind Cluster The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI). Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster). ## After Making Changes **After editing `*_types.go` or markers:** ``` make manifests # Regenerate CRDs/RBAC from markers make generate # Regenerate DeepCopy methods ``` **After editing `*.go` files:** ``` make lint-fix # Auto-fix code style make test # Run unit tests ``` ## CLI Commands Cheat Sheet ### Create API (your own types) ```bash kubebuilder create api --group --version --kind ``` ### Deploy Image Plugin (scaffold to deploy/manage ANY container image) Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.): ```bash # Example: deploying memcached kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \ --image=memcached:alpine \ --plugins=deploy-image.go.kubebuilder.io/v1-alpha ``` Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation. ### Create Webhooks ```bash # Validation + defaulting kubebuilder create webhook --group --version --kind \ --defaulting --programmatic-validation # Conversion webhook (for multi-version APIs) kubebuilder create webhook --group --version v1 --kind \ --conversion --spoke v2 ``` ### Controller for Core Kubernetes Types ```bash # Watch Pods kubebuilder create api --group core --version v1 --kind Pod \ --controller=true --resource=false # Watch Deployments kubebuilder create api --group apps --version v1 --kind Deployment \ --controller=true --resource=false ``` ### Controller for External Types (e.g., from other operators) Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.): ```bash # Example: watching cert-manager Certificate resources kubebuilder create api \ --group cert-manager --version v1 --kind Certificate \ --controller=true --resource=false \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=io \ --external-api-module=github.com/cert-manager/cert-manager ``` **Note:** Use `--external-api-module=@` only if you need a specific version. Otherwise, omit `@` to use what's in go.mod. ### Webhook for External Types ```bash # Example: validating external resources kubebuilder create webhook \ --group cert-manager --version v1 --kind Issuer \ --defaulting \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=io \ --external-api-module=github.com/cert-manager/cert-manager ``` ## Testing & Development ```bash make test # Run unit tests (uses envtest: real K8s API + etcd) make run # Run locally (uses current kubeconfig context) ``` Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup. ## Deployment Workflow ```bash # 1. Regenerate manifests make manifests generate # 2. Build & deploy export IMG=/:tag make docker-build docker-push IMG=$IMG # Or: kind load docker-image $IMG --name make deploy IMG=$IMG # 3. Test kubectl apply -k config/samples/ # 4. Debug kubectl logs -n -system deployment/-controller-manager -c manager -f ``` ### API Design **Key markers for** `api//*_types.go`: ```go // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Namespaced // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status" // On fields: // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:Pattern="^[a-z]+$" // +kubebuilder:default="value" ``` - **Use** `metav1.Condition` for status (not custom string fields) - **Use predefined types**: `metav1.Time` instead of `string` for dates - **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`) ### Controller Design **RBAC markers in** `internal/controller/*_controller.go`: ```go // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete ``` **Implementation rules:** - **Idempotent reconciliation**: Safe to run multiple times - **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts - **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)` - **Owner references**: Enable automatic garbage collection (`SetControllerReference`) - **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter` - **Finalizers**: Clean up external resources (buckets, VMs, DNS entries) ### Logging **Follow Kubernetes logging message style guidelines:** - Start from a capital letter - Do not end the message with a period - Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`) - Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"` - Specify object type: `"Deleted Pod"` not `"Deleted"` - Balanced key-value pairs ```go log.Info("Starting reconciliation") log.Info("Created Deployment", "name", deploy.Name) log.Error(err, "Failed to create Pod", "name", name) ``` **Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines ### Webhooks - **Create all types together**: `--defaulting --programmatic-validation --conversion` - **When`--force`is used**: Backup custom logic first, then restore after scaffolding - **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`) - Hub version: Usually oldest stable version (v1) - Spoke versions: Newer versions that convert to/from hub (v2, v3) - Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke) ### Learning from Examples The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation: ```bash kubebuilder create api --group example --version v1alpha1 --kind MyApp \ --image= --plugins=deploy-image.go.kubebuilder.io/v1-alpha ``` Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation. ## Distribution Options ### Option 1: YAML Bundle (Kustomize) ```bash # Generate dist/install.yaml from Kustomize manifests make build-installer IMG=/:tag ``` **Key points:** - The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment) - Commit this file to your repository for easy distribution - Users only need `kubectl` to install (no additional tools required) **Example:** Users install with a single command: ```bash kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml ``` ### Option 2: Helm Chart ```bash kubebuilder edit --plugins=helm/v2-alpha # Generates dist/chart/ (default) kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts # Generates charts/chart/ ``` **For development:** ```bash make helm-deploy IMG=/: # Deploy manager via Helm make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..." # Deploy with custom values make helm-status # Show release status make helm-uninstall # Remove release make helm-history # View release history make helm-rollback # Rollback to previous version ``` **For end users/production:** ```bash helm install my-release .//chart/ --namespace --create-namespace ``` **Important:** If you add webhooks or modify manifests after initial chart generation: 1. Backup any customizations in `/chart/values.yaml` and `/chart/manager/manager.yaml` 2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized) 3. Manually restore your custom values from the backup ### Publish Container Image ```bash export IMG=/: make docker-build docker-push IMG=$IMG ``` ## References ### Essential Reading - **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide) - **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions) - **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.) - **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels) ### API Design & Implementation - **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md - **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ - **Markers Reference**: https://book.kubebuilder.io/reference/markers.html ### Tools & Libraries - **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime - **controller-tools**: https://github.com/kubernetes-sigs/controller-tools - **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/Dockerfile ================================================ # Build the manager binary FROM golang:1.25 AS builder ARG TARGETOS ARG TARGETARCH WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the Go source (relies on .dockerignore to filter) COPY . . # Build # the GOARCH has no default value to allow the binary to be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 ENTRYPOINT ["/manager"] ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/Makefile ================================================ # Image URL to use all building/pushing image targets IMG ?= controller:latest # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif # CONTAINER_TOOL defines the container tool to be used for building images. # Be aware that the target commands are only tested with Docker which is # scaffolded by default. However, you might want to replace it to use other # tools. (i.e. podman) CONTAINER_TOOL ?= docker # Setting SHELL to bash allows bash commands to be executed by recipes. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec .PHONY: all all: build ##@ General # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the # target descriptions by '##'. The awk command is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. # More info on the usage of ANSI control characters for terminal formatting: # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters # More info on the awk command: # http://linuxcommand.org/lc3_adv_awk.php .PHONY: help help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ##@ Development .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. # Note that the option maxDescLen=0 was added in the default scaffold in order to sort out the issue # Too long: must have at most 262144 bytes. By using kubectl apply to create / update resources an annotation # is created by K8s API to store the latest version of the resource ( kubectl.kubernetes.io/last-applied-configuration). # However, it has a size limit and if the CRD is too big with so many long descriptions as this one it will cause the failure. "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. go fmt ./... .PHONY: vet vet: ## Run go vet against code. go vet ./... .PHONY: test test: manifests generate fmt vet setup-envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. # CertManager is installed by default; skip with: # - CERT_MANAGER_INSTALL_SKIP=true KIND_CLUSTER ?= project-test-e2e .PHONY: setup-test-e2e setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist @command -v $(KIND) >/dev/null 2>&1 || { \ echo "Kind is not installed. Please install Kind manually."; \ exit 1; \ } @case "$$($(KIND) get clusters)" in \ *"$(KIND_CLUSTER)"*) \ echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ *) \ echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ esac .PHONY: test-e2e test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v $(MAKE) cleanup-test-e2e .PHONY: cleanup-test-e2e cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests @$(KIND) delete cluster --name $(KIND_CLUSTER) .PHONY: lint lint: golangci-lint ## Run golangci-lint linter "$(GOLANGCI_LINT)" run .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes "$(GOLANGCI_LINT)" run --fix .PHONY: lint-config lint-config: golangci-lint ## Verify golangci-lint linter configuration "$(GOLANGCI_LINT)" config verify ##@ Build .PHONY: build build: manifests generate fmt vet ## Build manager binary. go build -o bin/manager cmd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. go run ./cmd/main.go # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build docker-build: ## Build docker image with the manager. $(CONTAINER_TOOL) build -t ${IMG} . .PHONY: docker-push docker-push: ## Push docker image with the manager. $(CONTAINER_TOOL) push ${IMG} # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le .PHONY: docker-buildx docker-buildx: ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - $(CONTAINER_TOOL) buildx create --name project-builder $(CONTAINER_TOOL) buildx use project-builder - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - $(CONTAINER_TOOL) buildx rm project-builder rm Dockerfile.cross .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} "$(KUSTOMIZE)" build config/default > dist/install.yaml ##@ Deployment ifndef ignore-not-found ignore-not-found = false endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f - ##@ Dependencies ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p "$(LOCALBIN)" ## Tool Binaries KUBECTL ?= kubectl KIND ?= kind KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions KUSTOMIZE_VERSION ?= v5.8.1 CONTROLLER_TOOLS_VERSION ?= v0.20.1 #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \ [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \ printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \ [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \ printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/') GOLANGCI_LINT_VERSION ?= v2.8.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. $(KUSTOMIZE): $(LOCALBIN) $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. $(CONTROLLER_GEN): $(LOCALBIN) $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) .PHONY: setup-envtest setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." @"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \ echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ exit 1; \ } .PHONY: envtest envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. $(ENVTEST): $(LOCALBIN) $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) @test -f .custom-gcl.yml && { \ echo "Building custom golangci-lint with plugins..." && \ $(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \ mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \ } || true # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed # $3 - specific version of package define go-install-tool @[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ set -e; \ package=$(2)@$(3) ;\ echo "Downloading $${package}" ;\ rm -f "$(1)" ;\ GOBIN="$(LOCALBIN)" go install $${package} ;\ mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\ } ;\ ln -sf "$$(realpath "$(1)-$(3)")" "$(1)" endef define gomodver $(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null) endef ##@ Helm Deployment ## Helm binary to use for deploying the chart HELM ?= helm ## Namespace to deploy the Helm release HELM_NAMESPACE ?= project-system ## Name of the Helm release HELM_RELEASE ?= project ## Path to the Helm chart directory HELM_CHART_DIR ?= dist/chart ## Additional arguments to pass to helm commands HELM_EXTRA_ARGS ?= .PHONY: install-helm install-helm: ## Install the latest version of Helm. @command -v $(HELM) >/dev/null 2>&1 || { \ echo "Installing Helm..." && \ curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \ } .PHONY: helm-deploy helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG. $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \ --namespace $(HELM_NAMESPACE) \ --create-namespace \ --set manager.image.repository=$${IMG%:*} \ --set manager.image.tag=$${IMG##*:} \ --wait \ --timeout 5m \ $(HELM_EXTRA_ARGS) .PHONY: helm-uninstall helm-uninstall: ## Uninstall the Helm release from the K8s cluster. $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-status helm-status: ## Show Helm release status. $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-history helm-history: ## Show Helm release history. $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-rollback helm-rollback: ## Rollback to previous Helm release. $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/PROJECT ================================================ # Code generated by tool. DO NOT EDIT. # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html cliVersion: (devel) domain: tutorial.kubebuilder.io layout: - go.kubebuilder.io/v4 plugins: helm.kubebuilder.io/v2-alpha: manifests: dist/install.yaml output: dist projectName: project repo: tutorial.kubebuilder.io/project resources: - api: crdVersion: v1 namespaced: true controller: true domain: tutorial.kubebuilder.io group: batch kind: CronJob path: tutorial.kubebuilder.io/project/api/v1 version: v1 webhooks: defaulting: true validation: true webhookVersion: v1 version: "3" ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/README.md ================================================ # project // TODO(user): Add simple overview of use/purpose ## Description // TODO(user): An in-depth paragraph about your project and overview of use ## Getting Started ### Prerequisites - go version v1.24.6+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. ### To Deploy on the cluster **Build and push your image to the location specified by `IMG`:** ```sh make docker-build docker-push IMG=/project:tag ``` **NOTE:** This image ought to be published in the personal registry you specified. And it is required to have access to pull the image from the working environment. Make sure you have the proper permission to the registry if the above commands don’t work. **Install the CRDs into the cluster:** ```sh make install ``` **Deploy the Manager to the cluster with the image specified by `IMG`:** ```sh make deploy IMG=/project:tag ``` > **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin privileges or be logged in as admin. **Create instances of your solution** You can apply the samples (examples) from the config/sample: ```sh kubectl apply -k config/samples/ ``` >**NOTE**: Ensure that the samples has default values to test it out. ### To Uninstall **Delete the instances (CRs) from the cluster:** ```sh kubectl delete -k config/samples/ ``` **Delete the APIs(CRDs) from the cluster:** ```sh make uninstall ``` **UnDeploy the controller from the cluster:** ```sh make undeploy ``` ## Project Distribution Following the options to release and provide this solution to the users. ### By providing a bundle with all YAML files 1. Build the installer for the image built and published in the registry: ```sh make build-installer IMG=/project:tag ``` **NOTE:** The makefile target mentioned above generates an 'install.yaml' file in the dist directory. This file contains all the resources built with Kustomize, which are necessary to install this project without its dependencies. 2. Using the installer Users can just run 'kubectl apply -f ' to install the project, i.e.: ```sh kubectl apply -f https://raw.githubusercontent.com//project//dist/install.yaml ``` ### By providing a Helm Chart 1. Build the chart using the optional helm plugin ```sh kubebuilder edit --plugins=helm/v2-alpha ``` 2. See that a chart was generated under 'dist/chart', and users can obtain this solution from there. **NOTE:** If you change the project, you need to update the Helm Chart using the same command above to sync the latest changes. Furthermore, if you create webhooks, you need to use the above command with the '--force' flag and manually ensure that any custom configuration previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' is manually re-applied afterwards. ## Contributing // TODO(user): Add detailed information on how you would like others to contribute to this project **NOTE:** Run `make help` for more information on all potential `make` targets More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) ## License Copyright 2026 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: docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_types.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* */ package v1 /* */ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // +kubebuilder:docs-gen:collapse=Imports /* First, let's take a look at our spec. As we discussed before, spec holds *desired state*, so any "inputs" to our controller go here. Fundamentally a CronJob needs the following pieces: - A schedule (the *cron* in CronJob) - A template for the Job to run (the *job* in CronJob) We'll also want a few extras, which will make our users' lives easier: - A deadline for starting jobs (if we miss this deadline, we'll just wait till the next scheduled time) - What to do if multiple jobs would run at once (do we wait? stop the old one? run both?) - A way to pause the running of a CronJob, in case something's wrong with it - Limits on old job history Remember, since we never read our own status, we need to have some other way to keep track of whether a job has run. We can use at least one old job to do this. We'll use several markers (`// +comment`) to specify additional metadata. These will be used by [controller-tools](https://github.com/kubernetes-sigs/controller-tools) when generating our CRD manifest. As we'll see in a bit, controller-tools will also use GoDoc to form descriptions for the fields. */ // CronJobSpec defines the desired state of CronJob type CronJobSpec struct { // schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. // +kubebuilder:validation:MinLength=0 // +required Schedule string `json:"schedule"` // startingDeadlineSeconds defines in seconds for starting the job if it misses scheduled // time for any reason. Missed jobs executions will be counted as failed ones. // +optional // +kubebuilder:validation:Minimum=0 StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"` // concurrencyPolicy specifies how to treat concurrent executions of a Job. // Valid values are: // - "Allow" (default): allows CronJobs to run concurrently; // - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; // - "Replace": cancels currently running job and replaces it with a new one // +optional // +kubebuilder:default:=Allow ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"` // suspend tells the controller to suspend subsequent executions, it does // not apply to already started executions. Defaults to false. // +optional Suspend *bool `json:"suspend,omitempty"` // jobTemplate defines the job that will be created when executing a CronJob. // +required JobTemplate batchv1.JobTemplateSpec `json:"jobTemplate"` // successfulJobsHistoryLimit defines the number of successful finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"` // failedJobsHistoryLimit defines the number of failed finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"` } /* We define a custom type to hold our concurrency policy. It's actually just a string under the hood, but the type gives extra documentation, and allows us to attach validation on the type instead of the field, making the validation more easily reusable. */ // ConcurrencyPolicy describes how the job will be handled. // Only one of the following concurrent policies may be specified. // If none of the following policies is specified, the default one // is AllowConcurrent. // +kubebuilder:validation:Enum=Allow;Forbid;Replace type ConcurrencyPolicy string const ( // AllowConcurrent allows CronJobs to run concurrently. AllowConcurrent ConcurrencyPolicy = "Allow" // ForbidConcurrent forbids concurrent runs, skipping next run if previous // hasn't finished yet. ForbidConcurrent ConcurrencyPolicy = "Forbid" // ReplaceConcurrent cancels currently running job and replaces it with a new one. ReplaceConcurrent ConcurrencyPolicy = "Replace" ) /* Next, let's design our status, which holds observed state. It contains any information we want users or other controllers to be able to easily obtain. We'll keep a list of actively running jobs, as well as the last time that we successfully ran our job. Notice that we use `metav1.Time` instead of `time.Time` to get the stable serialization, as mentioned above. */ // CronJobStatus defines the observed state of CronJob. type CronJobStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // active defines a list of pointers to currently running jobs. // +optional // +listType=atomic // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 Active []corev1.ObjectReference `json:"active,omitempty"` // lastScheduleTime defines when was the last time the job was successfully scheduled. // +optional LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"` // For Kubernetes API conventions, see: // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties // conditions represent the current state of the CronJob resource. // Each condition has a unique type and reflects the status of a specific aspect of the resource. // // Standard condition types include: // - "Available": the resource is fully functional // - "Progressing": the resource is being created or updated // - "Degraded": the resource failed to reach or maintain its desired state // // The status of each condition is one of True, False, or Unknown. // +listType=map // +listMapKey=type // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` } /* Finally, we have the rest of the boilerplate that we've already discussed. As previously noted, we don't need to change this, except to mark that we want a status subresource, so that we behave like built-in kubernetes types. */ // +kubebuilder:object:root=true // +kubebuilder:subresource:status // CronJob is the Schema for the cronjobs API type CronJob struct { /* */ metav1.TypeMeta `json:",inline"` // metadata is a standard object metadata // +optional metav1.ObjectMeta `json:"metadata,omitzero"` // spec defines the desired state of CronJob // +required Spec CronJobSpec `json:"spec"` // status defines the observed state of CronJob // +optional Status CronJobStatus `json:"status,omitzero"` } // +kubebuilder:object:root=true // CronJobList contains a list of CronJob type CronJobList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitzero"` Items []CronJob `json:"items"` } func init() { SchemeBuilder.Register(&CronJob{}, &CronJobList{}) } // +kubebuilder:docs-gen:collapse=Root Object Definitions ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/api/v1/groupversion_info.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* First, we have some *package-level* markers that denote that there are Kubernetes objects in this package, and that this package represents the group `batch.tutorial.kubebuilder.io`. The `object` generator makes use of the former, while the latter is used by the CRD generator to generate the right metadata for the CRDs it creates from this package. */ // Package v1 contains API Schema definitions for the batch v1 API group. // +kubebuilder:object:generate=true // +groupName=batch.tutorial.kubebuilder.io package v1 import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) /* Then, we have the commonly useful variables that help us set up our Scheme. Since we need to use all the types in this package in our controller, it's helpful (and the convention) to have a convenient method to add all the types to some other `Scheme`. SchemeBuilder makes this easy for us. */ var ( // SchemeGroupVersion is group version used to register these objects. // This name is used by applyconfiguration generators (e.g. controller-gen). SchemeGroupVersion = schema.GroupVersion{Group: "batch.tutorial.kubebuilder.io", Version: "v1"} // GroupVersion is an alias for SchemeGroupVersion, for backward compatibility. GroupVersion = SchemeGroupVersion // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2026 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 controller-gen. DO NOT EDIT. package v1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJob) DeepCopyInto(out *CronJob) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJob. func (in *CronJob) DeepCopy() *CronJob { if in == nil { return nil } out := new(CronJob) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *CronJob) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobList) DeepCopyInto(out *CronJobList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]CronJob, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobList. func (in *CronJobList) DeepCopy() *CronJobList { if in == nil { return nil } out := new(CronJobList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *CronJobList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobSpec) DeepCopyInto(out *CronJobSpec) { *out = *in if in.StartingDeadlineSeconds != nil { in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds *out = new(int64) **out = **in } if in.Suspend != nil { in, out := &in.Suspend, &out.Suspend *out = new(bool) **out = **in } in.JobTemplate.DeepCopyInto(&out.JobTemplate) if in.SuccessfulJobsHistoryLimit != nil { in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit *out = new(int32) **out = **in } if in.FailedJobsHistoryLimit != nil { in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit *out = new(int32) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSpec. func (in *CronJobSpec) DeepCopy() *CronJobSpec { if in == nil { return nil } out := new(CronJobSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobStatus) DeepCopyInto(out *CronJobStatus) { *out = *in if in.Active != nil { in, out := &in.Active, &out.Active *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } if in.LastScheduleTime != nil { in, out := &in.LastScheduleTime, &out.LastScheduleTime *out = (*in).DeepCopy() } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobStatus. func (in *CronJobStatus) DeepCopy() *CronJobStatus { if in == nil { return nil } out := new(CronJobStatus) in.DeepCopyInto(out) return out } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/cmd/main.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package main import ( "crypto/tls" "flag" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" batchv1 "tutorial.kubebuilder.io/project/api/v1" "tutorial.kubebuilder.io/project/internal/controller" webhookv1 "tutorial.kubebuilder.io/project/internal/webhook/v1" // +kubebuilder:scaffold:imports ) // +kubebuilder:docs-gen:collapse=Imports /* The first difference to notice is that kubebuilder has added the new API group's package (`batchv1`) to our scheme. This means that we can use those objects in our controller. If we would be using any other CRD we would have to add their scheme the same way. Builtin types such as Job have their scheme added by `clientgoscheme`. */ var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(batchv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } /* The other thing that's changed is that kubebuilder has added a block calling our CronJob controller's `SetupWithManager` method. */ // nolint:gocyclo func main() { /* */ var metricsAddr string var metricsCertPath, metricsCertName, metricsCertKey string var webhookCertPath, webhookCertName, webhookCertKey string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") flag.StringVar(&metricsCertPath, "metrics-cert-path", "", "The directory that contains the metrics server certificate.") flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and // Rapid Reset CVEs. For more information see: // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 // - https://github.com/advisories/GHSA-4374-p667-p6c8 disableHTTP2 := func(c *tls.Config) { setupLog.Info("Disabling HTTP/2") c.NextProtos = []string{"http/1.1"} } if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) } // Initial webhook TLS options webhookTLSOpts := tlsOpts webhookServerOptions := webhook.Options{ TLSOpts: webhookTLSOpts, } if len(webhookCertPath) > 0 { setupLog.Info("Initializing webhook certificate watcher using provided certificates", "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) webhookServerOptions.CertDir = webhookCertPath webhookServerOptions.CertName = webhookCertName webhookServerOptions.KeyName = webhookCertKey } webhookServer := webhook.NewServer(webhookServerOptions) // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, SecureServing: secureMetrics, TLSOpts: tlsOpts, } if secureMetrics { // FilterProvider is used to protect the metrics endpoint with authn/authz. // These configurations ensure that only authorized users and service accounts // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/filters#WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } // If the certificate is not specified, controller-runtime will automatically // generate self-signed certificates for the metrics server. While convenient for development and testing, // this setup is not recommended for production. // // TODO(user): If you enable certManager, uncomment the following lines: // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates // managed by cert-manager for the metrics server. // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. if len(metricsCertPath) > 0 { setupLog.Info("Initializing metrics certificate watcher using provided certificates", "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) metricsServerOptions.CertDir = metricsCertPath metricsServerOptions.CertName = metricsCertName metricsServerOptions.KeyName = metricsCertKey } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "80807133.tutorial.kubebuilder.io", // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily // when the Manager ends. This requires the binary to immediately end when the // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly // speeds up voluntary leader transitions as the new leader don't have to wait // LeaseDuration time first. // // In the default scaffold provided, the program ends immediately after // the manager stops, so would be fine to enable this option. However, // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, }) if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) } // +kubebuilder:docs-gen:collapse=Remaining code from main.go if err := (&controller.CronJobReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "CronJob") os.Exit(1) } /* We'll also set up webhooks for our type, which we'll talk about next. We just need to add them to the manager. Since we might want to run the webhooks separately, or not run them when testing our controller locally, we'll put them behind an environment variable. We'll just make sure to set `ENABLE_WEBHOOKS=false` when we run locally. */ // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err := webhookv1.SetupCronJobWebhookWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create webhook", "webhook", "CronJob") os.Exit(1) } } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up ready check") os.Exit(1) } setupLog.Info("Starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "Failed to run manager") os.Exit(1) } } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/certmanager/certificate-metrics.yaml ================================================ # The following manifests contain a self-signed issuer CR and a metrics certificate CR. # More document can be found at https://docs.cert-manager.io apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: metrics-certs # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: dnsNames: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize # replacements in the config/default/kustomization.yaml file. - SERVICE_NAME.SERVICE_NAMESPACE.svc - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer secretName: metrics-server-cert ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/certmanager/certificate-webhook.yaml ================================================ # The following manifests contain a self-signed issuer CR and a certificate CR. # More document can be found at https://docs.cert-manager.io apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize # replacements in the config/default/kustomization.yaml file. dnsNames: - SERVICE_NAME.SERVICE_NAMESPACE.svc - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer secretName: webhook-server-cert ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/certmanager/issuer.yaml ================================================ # The following manifest contains a self-signed issuer CR. # More information can be found at https://docs.cert-manager.io # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. apiVersion: cert-manager.io/v1 kind: Issuer metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: selfsigned-issuer namespace: system spec: selfSigned: {} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/certmanager/kustomization.yaml ================================================ resources: - issuer.yaml - certificate-webhook.yaml - certificate-metrics.yaml configurations: - kustomizeconfig.yaml ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/certmanager/kustomizeconfig.yaml ================================================ # This configuration is for teaching kustomize how to update name ref substitution nameReference: - kind: Issuer group: cert-manager.io fieldSpecs: - kind: Certificate group: cert-manager.io path: spec/issuerRef/name ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/crd/bases/batch.tutorial.kubebuilder.io_cronjobs.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.20.1 name: cronjobs.batch.tutorial.kubebuilder.io spec: group: batch.tutorial.kubebuilder.io names: kind: CronJob listKind: CronJobList plural: cronjobs singular: cronjob scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: minLength: 0 type: string startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: true subresources: status: {} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/crd/kustomization.yaml ================================================ # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: - bases/batch.tutorial.kubebuilder.io_cronjobs.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD # +kubebuilder:scaffold:crdkustomizewebhookpatch # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. #configurations: #- kustomizeconfig.yaml ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/crd/kustomizeconfig.yaml ================================================ # This file is for teaching kustomize how to substitute name and namespace reference in CRD nameReference: - kind: Service version: v1 fieldSpecs: - kind: CustomResourceDefinition version: v1 group: apiextensions.k8s.io path: spec/conversion/webhook/clientConfig/service/name varReference: - path: metadata/annotations ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/default/cert_metrics_manager_patch.yaml ================================================ # This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. # Add the volumeMount for the metrics-server certs - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true # Add the --metrics-cert-path argument for the metrics server - op: add path: /spec/template/spec/containers/0/args/- value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs # Add the metrics-server certs volume configuration - op: add path: /spec/template/spec/volumes/- value: name: metrics-certs secret: secretName: metrics-server-cert optional: false items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/default/kustomization.yaml ================================================ # Adds namespace to all resources. namespace: project-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: project- # Labels to add to all resources and selectors. #labels: #- includeSelectors: true # pairs: # someName: someValue resources: - ../crd - ../rbac - ../manager # ANCHOR: webhook-resources # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # ANCHOR_END: webhook-resources # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. - ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will # be able to communicate with the Webhook Server. #- ../network-policy # Uncomment the patches line if you enable Metrics patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: kind: Deployment # Uncomment the patches line if you enable Metrics and CertManager # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. # This patch will protect the metrics with certManager self-signed certs. - path: cert_metrics_manager_patch.yaml target: kind: Deployment # ANCHOR: webhook-patch # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - path: manager_webhook_patch.yaml target: kind: Deployment # ANCHOR_END: webhook-patch # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations replacements: - source: # Uncomment the following block to enable certificates for metrics kind: Service version: v1 name: controller-manager-metrics-service fieldPath: metadata.name targets: - select: kind: Certificate group: cert-manager.io version: v1 name: metrics-certs fieldPaths: - spec.dnsNames.0 - spec.dnsNames.1 options: delimiter: '.' index: 0 create: true - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor kind: ServiceMonitor group: monitoring.coreos.com version: v1 name: controller-manager-metrics-monitor fieldPaths: - spec.endpoints.0.tlsConfig.serverName options: delimiter: '.' index: 0 create: true - source: kind: Service version: v1 name: controller-manager-metrics-service fieldPath: metadata.namespace targets: - select: kind: Certificate group: cert-manager.io version: v1 name: metrics-certs fieldPaths: - spec.dnsNames.0 - spec.dnsNames.1 options: delimiter: '.' index: 1 create: true - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor kind: ServiceMonitor group: monitoring.coreos.com version: v1 name: controller-manager-metrics-monitor fieldPaths: - spec.endpoints.0.tlsConfig.serverName options: delimiter: '.' index: 1 create: true # ANCHOR: webhook-replacements - source: # Uncomment the following block if you have any webhook kind: Service version: v1 name: webhook-service fieldPath: .metadata.name # Name of the service targets: - select: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPaths: - .spec.dnsNames.0 - .spec.dnsNames.1 options: delimiter: '.' index: 0 create: true - source: kind: Service version: v1 name: webhook-service fieldPath: .metadata.namespace # Namespace of the service targets: - select: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPaths: - .spec.dnsNames.0 - .spec.dnsNames.1 options: delimiter: '.' index: 1 create: true - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) kind: Certificate group: cert-manager.io version: v1 name: serving-cert # This name should match the one in certificate.yaml fieldPath: .metadata.namespace # Namespace of the certificate CR targets: - select: kind: ValidatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 0 create: true - source: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.name targets: - select: kind: ValidatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 1 create: true - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.namespace # Namespace of the certificate CR targets: - select: kind: MutatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 0 create: true - source: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.name targets: - select: kind: MutatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 1 create: true # ANCHOR_END: webhook-replacements # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectionns # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectionname ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/default/manager_metrics_patch.yaml ================================================ # This patch adds the args to allow exposing the metrics endpoint using HTTPS - op: add path: /spec/template/spec/containers/0/args/0 value: --metrics-bind-address=:8443 ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/default/manager_webhook_patch.yaml ================================================ # This patch ensures the webhook certificates are properly mounted in the manager container. # It configures the necessary arguments, volumes, volume mounts, and container ports. # Add the --webhook-cert-path argument for configuring the webhook certificate path - op: add path: /spec/template/spec/containers/0/args/- value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs # Add the volumeMount for the webhook certificates - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs readOnly: true # Add the port configuration for the webhook server - op: add path: /spec/template/spec/containers/0/ports/- value: containerPort: 9443 name: webhook-server protocol: TCP # Add the volume configuration for the webhook certificates - op: add path: /spec/template/spec/volumes/- value: name: webhook-certs secret: secretName: webhook-server-cert ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/default/metrics_service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-service namespace: system spec: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 selector: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/manager/kustomization.yaml ================================================ resources: - manager.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller newName: controller newTag: latest ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/manager/manager.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: system --- apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize spec: selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: control-plane: controller-manager app.kubernetes.io/name: project spec: # TODO(user): Uncomment the following code to configure the nodeAffinity expression # according to the platforms which are supported by your solution. # It is considered best practice to support multiple architectures. You can # build your manager image using the makefile target docker-buildx. # affinity: # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: kubernetes.io/arch # operator: In # values: # - amd64 # - arm64 # - ppc64le # - s390x # - key: kubernetes.io/os # operator: In # values: # - linux securityContext: # Projects are configured by default to adhere to the "restricted" Pod Security Standards. # This ensures that deployments meet the highest security requirements for Kubernetes. # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - command: - /manager args: - --leader-elect - --health-probe-bind-address=:8081 image: controller:latest name: manager ports: [] securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: - "ALL" livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 # TODO(user): Configure the resources accordingly based on the project requirements. # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi volumeMounts: [] volumes: [] serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/network-policy/allow-metrics-traffic.yaml ================================================ # This NetworkPolicy allows ingress traffic # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those # namespaces are able to gather data from the metrics endpoint. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: allow-metrics-traffic namespace: system spec: podSelector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project policyTypes: - Ingress ingress: # This allows ingress traffic from any namespace with the label metrics: enabled - from: - namespaceSelector: matchLabels: metrics: enabled # Only from namespaces with this label ports: - port: 8443 protocol: TCP ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/network-policy/allow-webhook-traffic.yaml ================================================ # This NetworkPolicy allows ingress traffic to your webhook server running # as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks # will only work when applied in namespaces labeled with 'webhook: enabled' apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: allow-webhook-traffic namespace: system spec: podSelector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project policyTypes: - Ingress ingress: # This allows ingress traffic from any namespace with the label webhook: enabled - from: - namespaceSelector: matchLabels: webhook: enabled # Only from namespaces with this label ports: - port: 443 protocol: TCP ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/network-policy/kustomization.yaml ================================================ resources: - allow-webhook-traffic.yaml - allow-metrics-traffic.yaml ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/prometheus/kustomization.yaml ================================================ resources: - monitor.yaml # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus # to securely reference certificates created and managed by cert-manager. # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml # to mount the "metrics-server-cert" secret in the Manager Deployment. patches: - path: monitor_tls_patch.yaml target: kind: ServiceMonitor ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/prometheus/monitor.yaml ================================================ # Prometheus Monitor Service (Metrics) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system spec: endpoints: - path: /metrics port: https # Ensure this is the name of the port that exposes HTTPS metrics scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables # certificate verification, exposing the system to potential man-in-the-middle attacks. # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, # which securely references the certificate from the 'metrics-server-cert' secret. insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/prometheus/monitor_tls_patch.yaml ================================================ # Patch for Prometheus ServiceMonitor to enable secure TLS configuration # using certificates managed by cert-manager - op: replace path: /spec/endpoints/0/tlsConfig value: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc insecureSkipVerify: false ca: secret: name: metrics-server-cert key: ca.crt cert: secret: name: metrics-server-cert key: tls.crt keySecret: name: metrics-server-cert key: tls.key ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/cronjob_admin_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants full permissions ('*') over batch.tutorial.kubebuilder.io. # This role is intended for users authorized to modify roles and bindings within the cluster, # enabling them to delegate specific permissions to other users or groups as needed. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-admin-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - '*' - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/cronjob_editor_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants permissions to create, update, and delete resources within the batch.tutorial.kubebuilder.io. # This role is intended for users who need to manage these resources # but should not control RBAC or manage permissions for others. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-editor-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/cronjob_viewer_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants read-only access to batch.tutorial.kubebuilder.io resources. # This role is intended for users who need visibility into these resources # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-viewer-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - get - list - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/kustomization.yaml ================================================ resources: # All RBAC will be applied under this service account in # the deployment namespace. You may comment out this resource # if your manager will use a service account that exists at # runtime. Be sure to update RoleBinding and ClusterRoleBinding # subjects if changing service account names. - service_account.yaml - role.yaml - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # The following RBAC configurations are used to protect # the metrics endpoint with authn/authz. These configurations # ensure that only authorized users and service accounts # can access the metrics endpoint. Comment the following # permissions if you want to disable this protection. # More info: https://book.kubebuilder.io/reference/metrics.html - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by # default, aiding admins in cluster management. Those roles are # not used by the project itself. You can comment the following lines # if you do not want those helpers be installed with your Project. - cronjob_admin_role.yaml - cronjob_editor_role.yaml - cronjob_viewer_role.yaml ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/leader_election_role.yaml ================================================ # permissions to do leader election. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/leader_election_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: leader-election-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/metrics_auth_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-auth-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/metrics_auth_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: metrics-auth-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: metrics-auth-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/metrics_reader_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-reader rules: - nonResourceURLs: - "/metrics" verbs: - get ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/role.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: manager-role rules: - apiGroups: - batch resources: - jobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch resources: - jobs/status verbs: - get - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/finalizers verbs: - update - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get - patch - update ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: manager-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/rbac/service_account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager namespace: system ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/samples/batch_v1_cronjob.yaml ================================================ apiVersion: batch.tutorial.kubebuilder.io/v1 kind: CronJob metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-sample spec: schedule: "*/1 * * * *" startingDeadlineSeconds: 60 concurrencyPolicy: Allow # explicitly specify, but Allow is also default. jobTemplate: spec: template: spec: securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false restartPolicy: OnFailure ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/samples/kustomization.yaml ================================================ ## Append samples of your project ## resources: - batch_v1_cronjob.yaml # +kubebuilder:scaffold:manifestskustomizesamples ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/webhook/kustomization.yaml ================================================ resources: - manifests.yaml - service.yaml ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/webhook/manifests.yaml ================================================ --- apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: system path: /mutate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: mcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: system path: /validate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: vcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/config/webhook/service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: webhook-service namespace: system spec: ports: - port: 443 protocol: TCP targetPort: 9443 selector: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/.helmignore ================================================ # Patterns to ignore when building Helm packages. # Operating system files .DS_Store # Version control directories .git/ .gitignore .bzr/ .hg/ .hgignore .svn/ # Backup and temporary files *.swp *.tmp *.bak *.orig *~ # IDE and editor-related files .idea/ .vscode/ # Helm chart artifacts dist/chart/*.tgz ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/Chart.yaml ================================================ apiVersion: v2 name: project description: A Helm chart to distribute project type: application version: 0.1.0 appVersion: "0.1.0" keywords: - kubernetes - operator annotations: kubebuilder.io/generated-by: kubebuilder ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/NOTES.txt ================================================ Thank you for installing {{ .Chart.Name }}. Your release is named {{ .Release.Name }}. The controller and CRDs have been installed in namespace {{ .Release.Namespace }}. To verify the installation: kubectl get pods -n {{ .Release.Namespace }} kubectl get customresourcedefinitions To learn more about the release, try: $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "project.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "project.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Namespace for generated references. Always uses the Helm release namespace. */}} {{- define "project.namespaceName" -}} {{- .Release.Namespace }} {{- end }} {{/* Resource name with proper truncation for Kubernetes 63-character limit. Takes a dict with: - .suffix: Resource name suffix (e.g., "metrics", "webhook") - .context: Template context (root context with .Values, .Release, etc.) Dynamically calculates safe truncation to ensure total name length <= 63 chars. */}} {{- define "project.resourceName" -}} {{- $fullname := include "project.fullname" .context }} {{- $suffix := .suffix }} {{- $maxLen := sub 62 (len $suffix) | int }} {{- if gt (len $fullname) $maxLen }} {{- printf "%s-%s" (trunc $maxLen $fullname | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" $fullname $suffix | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/cert-manager/metrics-certs.yaml ================================================ {{- if and .Values.certManager.enable .Values.metrics.enable }} apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "metrics-certs" "context" $) }} namespace: {{ .Release.Namespace }} spec: dnsNames: - {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc - {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local issuerRef: kind: Issuer name: {{ include "project.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} secretName: metrics-server-cert {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/cert-manager/selfsigned-issuer.yaml ================================================ {{- if .Values.certManager.enable }} apiVersion: cert-manager.io/v1 kind: Issuer metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} namespace: {{ .Release.Namespace }} spec: selfSigned: {} {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/cert-manager/serving-cert.yaml ================================================ {{- if .Values.certManager.enable }} apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "serving-cert" "context" $) }} namespace: {{ .Release.Namespace }} spec: dnsNames: - {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc - {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local issuerRef: kind: Issuer name: {{ include "project.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} secretName: webhook-server-cert {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/crd/cronjobs.batch.tutorial.kubebuilder.io.yaml ================================================ {{- if .Values.crd.enable }} apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: {{- if .Values.crd.keep }} "helm.sh/resource-policy": keep {{- end }} controller-gen.kubebuilder.io/version: v0.20.1 name: cronjobs.batch.tutorial.kubebuilder.io spec: group: batch.tutorial.kubebuilder.io names: kind: CronJob listKind: CronJobList plural: cronjobs singular: cronjob scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: minLength: 0 type: string startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: true subresources: status: {} {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} spec: replicas: {{ .Values.manager.replicas }} selector: matchLabels: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} control-plane: controller-manager spec: {{- with .Values.manager.tolerations }} tolerations: {{ toYaml . | nindent 10 }} {{- end }} {{- with .Values.manager.affinity }} affinity: {{ toYaml . | nindent 10 }} {{- end }} {{- with .Values.manager.nodeSelector }} nodeSelector: {{ toYaml . | nindent 10 }} {{- end }} containers: - args: {{- if .Values.metrics.enable }} - --metrics-bind-address=:{{ .Values.metrics.port }} {{- else }} # Bind to :0 to disable the controller-runtime managed metrics server - --metrics-bind-address=0 {{- end }} - --health-probe-bind-address=:8081 {{- range .Values.manager.args }} - {{ . }} {{- end }} {{- if and .Values.certManager.enable .Values.metrics.enable }} - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs {{- end }} {{- if .Values.certManager.enable }} - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs {{- end }} command: - /manager image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}" imagePullPolicy: {{ .Values.manager.image.pullPolicy }} livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 name: manager ports: - containerPort: {{ .Values.webhook.port }} name: webhook-server protocol: TCP readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 resources: {{- if .Values.manager.resources }} {{- toYaml .Values.manager.resources | nindent 10 }} {{- else }} {} {{- end }} securityContext: {{- if .Values.manager.securityContext }} {{- toYaml .Values.manager.securityContext | nindent 10 }} {{- else }} {} {{- end }} volumeMounts: {{- if and .Values.certManager.enable .Values.metrics.enable }} - mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true {{- end }} {{- if .Values.certManager.enable }} - mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs readOnly: true {{- end }} securityContext: {{- if .Values.manager.podSecurityContext }} {{- toYaml .Values.manager.podSecurityContext | nindent 8 }} {{- else }} {} {{- end }} serviceAccountName: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} terminationGracePeriodSeconds: 10 volumes: {{- if and .Values.certManager.enable .Values.metrics.enable }} - name: metrics-certs secret: items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key optional: false secretName: metrics-server-cert {{- end }} {{- if .Values.certManager.enable }} - name: webhook-certs secret: secretName: webhook-server-cert {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }} namespace: {{ .Release.Namespace }} spec: ports: - name: https port: {{ .Values.metrics.port }} protocol: TCP targetPort: {{ .Values.metrics.port }} selector: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/prometheus/controller-manager-metrics-monitor.yaml ================================================ {{- if .Values.prometheus.enable }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-monitor" "context" $) }} namespace: {{ .Release.Namespace }} spec: endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token path: /metrics port: https scheme: https tlsConfig: ca: secret: key: ca.crt name: metrics-server-cert cert: secret: key: tls.crt name: metrics-server-cert insecureSkipVerify: false keySecret: key: tls.key name: metrics-server-cert serverName: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc selector: matchLabels: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/controller-manager.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/cronjob-admin-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "cronjob-admin-role" "context" $) }} rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - '*' - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/cronjob-editor-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "cronjob-editor-role" "context" $) }} rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/cronjob-viewer-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "cronjob-viewer-role" "context" $) }} rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - get - list - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/leader-election-role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "leader-election-role" "context" $) }} namespace: {{ .Release.Namespace }} rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/leader-election-rolebinding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "leader-election-rolebinding" "context" $) }} namespace: {{ .Release.Namespace }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ include "project.resourceName" (dict "suffix" "leader-election-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/manager-role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "manager-role" "context" $) }} rules: - apiGroups: - batch resources: - jobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch resources: - jobs/status verbs: - get - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/finalizers verbs: - update - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get - patch - update ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/manager-rolebinding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "manager-rolebinding" "context" $) }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ include "project.resourceName" (dict "suffix" "manager-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/metrics-auth-role.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/metrics-auth-rolebinding.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-rolebinding" "context" $) }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/rbac/metrics-reader.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-reader" "context" $) }} rules: - nonResourceURLs: - /metrics verbs: - get {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/webhook/mutating-webhook-configuration.yaml ================================================ {{- if .Values.webhook.enable }} apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: annotations: {{- if .Values.certManager.enable }} cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "project.resourceName" (dict "suffix" "serving-cert" "context" $) }} {{- end }} name: {{ include "project.resourceName" (dict "suffix" "mutating-webhook-configuration" "context" $) }} webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} path: /mutate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: mcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/webhook/validating-webhook-configuration.yaml ================================================ {{- if .Values.webhook.enable }} apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: annotations: {{- if .Values.certManager.enable }} cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "project.resourceName" (dict "suffix" "serving-cert" "context" $) }} {{- end }} name: {{ include "project.resourceName" (dict "suffix" "validating-webhook-configuration" "context" $) }} webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} path: /validate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: vcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml ================================================ {{- if .Values.webhook.enable }} apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} spec: ports: - port: 443 protocol: TCP targetPort: {{ .Values.webhook.port }} selector: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/chart/values.yaml ================================================ ## String to partially override chart.fullname template (will maintain the release name) ## # nameOverride: "" ## String to fully override chart.fullname template ## # fullnameOverride: "" ## Configure the controller manager deployment ## manager: replicas: 1 image: repository: controller tag: latest pullPolicy: IfNotPresent ## Arguments ## args: - --leader-elect ## Environment variables ## env: [] ## Env overrides (--set manager.envOverrides.VAR=value) ## Same name in env above: this value takes precedence. ## envOverrides: {} ## Image pull secrets ## imagePullSecrets: [] ## Pod-level security settings ## podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault ## Container-level security settings ## securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true ## Resource limits and requests ## resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi ## Manager pod's affinity ## affinity: {} ## Manager pod's node selector ## nodeSelector: {} ## Manager pod's tolerations ## tolerations: [] ## Helper RBAC roles for managing custom resources ## rbacHelpers: # Install convenience admin/editor/viewer roles for CRDs enable: false ## Custom Resource Definitions ## crd: # Install CRDs with the chart enable: true # Keep CRDs when uninstalling keep: true ## Controller metrics endpoint. ## Enable to expose /metrics endpoint with RBAC protection. ## metrics: enable: true # Metrics server port port: 8443 ## Cert-manager integration for TLS certificates. ## Required for webhook certificates and metrics endpoint certificates. ## certManager: enable: true ## Webhook server configuration ## webhook: enable: true # Webhook server port port: 9443 ## Prometheus ServiceMonitor for metrics scraping. ## Requires prometheus-operator to be installed in the cluster. ## prometheus: enable: false ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/dist/install.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-system --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.20.1 name: cronjobs.batch.tutorial.kubebuilder.io spec: group: batch.tutorial.kubebuilder.io names: kind: CronJob listKind: CronJobList plural: cronjobs singular: cronjob scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: minLength: 0 type: string startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: true subresources: status: {} --- apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-leader-election-role namespace: project-system rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-cronjob-admin-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - '*' - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-cronjob-editor-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-cronjob-viewer-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - get - list - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-manager-role rules: - apiGroups: - batch resources: - jobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch resources: - jobs/status verbs: - get - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/finalizers verbs: - update - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get - patch - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-metrics-auth-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-metrics-reader rules: - nonResourceURLs: - /metrics verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-leader-election-rolebinding namespace: project-system roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: project-leader-election-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: project-manager-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: project-metrics-auth-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: project-metrics-auth-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager-metrics-service namespace: project-system spec: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 selector: app.kubernetes.io/name: project control-plane: controller-manager --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-webhook-service namespace: project-system spec: ports: - port: 443 protocol: TCP targetPort: 9443 selector: app.kubernetes.io/name: project control-plane: controller-manager --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager namespace: project-system spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: project control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: app.kubernetes.io/name: project control-plane: controller-manager spec: containers: - args: - --metrics-bind-address=:8443 - --leader-elect - --health-probe-bind-address=:8081 - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs command: - /manager image: controller:latest livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 name: manager ports: - containerPort: 9443 name: webhook-server protocol: TCP readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true volumeMounts: - mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true - mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs readOnly: true securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault serviceAccountName: project-controller-manager terminationGracePeriodSeconds: 10 volumes: - name: metrics-certs secret: items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key optional: false secretName: metrics-server-cert - name: webhook-certs secret: secretName: webhook-server-cert --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-metrics-certs namespace: project-system spec: dnsNames: - project-controller-manager-metrics-service.project-system.svc - project-controller-manager-metrics-service.project-system.svc.cluster.local issuerRef: kind: Issuer name: project-selfsigned-issuer secretName: metrics-server-cert --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-serving-cert namespace: project-system spec: dnsNames: - project-webhook-service.project-system.svc - project-webhook-service.project-system.svc.cluster.local issuerRef: kind: Issuer name: project-selfsigned-issuer secretName: webhook-server-cert --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-selfsigned-issuer namespace: project-system spec: selfSigned: {} --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager-metrics-monitor namespace: project-system spec: endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token path: /metrics port: https scheme: https tlsConfig: ca: secret: key: ca.crt name: metrics-server-cert cert: secret: key: tls.crt name: metrics-server-cert insecureSkipVerify: false keySecret: key: tls.key name: metrics-server-cert serverName: project-controller-manager-metrics-service.project-system.svc selector: matchLabels: app.kubernetes.io/name: project control-plane: controller-manager --- apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: annotations: cert-manager.io/inject-ca-from: project-system/project-serving-cert name: project-mutating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: project-webhook-service namespace: project-system path: /mutate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: mcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: annotations: cert-manager.io/inject-ca-from: project-system/project-serving-cert name: project-validating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: project-webhook-service namespace: project-system path: /validate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: vcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/go.mod ================================================ module tutorial.kubebuilder.io/project go 1.25.3 require ( github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/robfig/cron v1.2.0 k8s.io/api v0.35.0 k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.23.3 ) require ( cel.dev/expr v0.24.0 // indirect github.com/Masterminds/semver/v3 v3.4.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.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // 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-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.3 // 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/pprof v0.0.0-20250403155104-27863c87afa6 // 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.0 // 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.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.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.29.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.38.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.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.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.0 // 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/kube-openapi v0.0.0-20250910181357-589584f1c912 // 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.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.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 v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 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.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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.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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-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/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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 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.1/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/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.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/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 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/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.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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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/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/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/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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 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-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.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/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 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/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= 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.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/hack/boilerplate.go.txt ================================================ /* Copyright 2026 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: docs/book/src/cronjob-tutorial/testdata/project/internal/controller/cronjob_controller.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* We'll start out with some imports. You'll see below that we'll need a few more imports than those scaffolded for us. We'll talk about each one when we use it. */ package controller import ( "context" "fmt" "maps" "slices" "time" "github.com/robfig/cron" kbatch "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" 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" ref "k8s.io/client-go/tools/reference" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) /* Next, we'll need a Clock, which will allow us to fake timing in our tests. */ // CronJobReconciler reconciles a CronJob object type CronJobReconciler struct { client.Client Scheme *runtime.Scheme Clock } /* We'll mock out the clock to make it easier to jump around in time while testing, the "real" clock just calls `time.Now`. */ type realClock struct{} func (_ realClock) Now() time.Time { return time.Now() } //nolint:staticcheck // Clock knows how to get the current time. // It can be used to fake out timing for testing. type Clock interface { Now() time.Time } // +kubebuilder:docs-gen:collapse=Clock Code Implementation // Definitions to manage status conditions const ( // typeAvailableCronJob represents the status of the CronJob reconciliation typeAvailableCronJob = "Available" // typeProgressingCronJob represents the status used when the CronJob is being reconciled typeProgressingCronJob = "Progressing" // typeDegradedCronJob represents the status used when the CronJob has encountered an error typeDegradedCronJob = "Degraded" ) /* Notice that we need a few more RBAC permissions -- since we're creating and managing jobs now, we'll need permissions for those, which means adding a couple more [markers](/reference/markers/rbac.md). */ // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get /* Now, we get to the heart of the controller -- the reconciler logic. */ var ( scheduledTimeAnnotation = "batch.tutorial.kubebuilder.io/scheduled-at" ) // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by // the CronJob object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile // nolint:gocyclo func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logf.FromContext(ctx) /* ### 1: Load the CronJob by name We'll fetch the CronJob using our client. All client methods take a context (to allow for cancellation) as their first argument, and the object in question as their last. Get is a bit special, in that it takes a [`NamespacedName`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client?tab=doc#ObjectKey) as the middle argument (most don't have a middle argument, as we'll see below). Many client methods also take variadic options at the end. */ var cronJob batchv1.CronJob if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { if apierrors.IsNotFound(err) { // If the custom resource is not found then it usually means that it was deleted or not created // In this way, we will stop the reconciliation log.Info("CronJob resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get CronJob") return ctrl.Result{}, err } // Initialize status conditions if not yet present if len(cronJob.Status.Conditions) == 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation", }) if err := r.Status().Update(ctx, &cronJob); err != nil { log.Error(err, "Failed to update CronJob status") return ctrl.Result{}, err } /* After updating the status, we re-fetch the CronJob to ensure we are working with the latest version of the object from the API server. Kubernetes uses optimistic concurrency, meaning that any update (including a status update) may change the resource version. If we continue reconciliation with a stale copy, subsequent updates may fail with a conflict such as: "the object has been modified; please apply your changes to the latest version and try again". By re-fetching here, we keep our reconciliation logic in sync with the actual cluster state and avoid unnecessary conflicts and requeues. */ if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { log.Error(err, "Failed to re-fetch CronJob") return ctrl.Result{}, err } } /* ### 2: List all active jobs, and update the status To fully update our status, we'll need to list all child jobs in this namespace that belong to this CronJob. Similarly to Get, we can use the List method to list the child jobs. Notice that we use variadic options to set the namespace and field match (which is actually an index lookup that we set up below). */ var childJobs kbatch.JobList if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: req.Name}); err != nil { log.Error(err, "unable to list child Jobs") /* Before updating, ensure we have the latest state of the resource to avoid conflict errors (e.g. "the object has been modified") that would re-trigger the reconcile loop. */ if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "ReconciliationError", Message: fmt.Sprintf("Failed to list child jobs: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return ctrl.Result{}, err } /* Once we have all the jobs we own, we'll split them into active, successful, and failed jobs, keeping track of the most recent run so that we can record it in status. Remember, status should be able to be reconstituted from the state of the world, so it's generally not a good idea to read from the status of the root object. Instead, you should reconstruct it every run. That's what we'll do here. We can check if a job is "finished" and whether it succeeded or failed using status conditions. We'll put that logic in a helper to make our code cleaner. */ // find the active list of jobs var activeJobs []*kbatch.Job var successfulJobs []*kbatch.Job var failedJobs []*kbatch.Job var mostRecentTime *time.Time // find the last run so we can update the status /* We consider a job "finished" if it has a "Complete" or "Failed" condition marked as true. Status conditions allow us to add extensible status information to our objects that other humans and controllers can examine to check things like completion and health. */ isJobFinished := func(job *kbatch.Job) (bool, kbatch.JobConditionType) { for _, c := range job.Status.Conditions { if (c.Type == kbatch.JobComplete || c.Type == kbatch.JobFailed) && c.Status == corev1.ConditionTrue { return true, c.Type } } return false, "" } // +kubebuilder:docs-gen:collapse=isJobFinished /* We'll use a helper to extract the scheduled time from the annotation that we added during job creation. */ getScheduledTimeForJob := func(job *kbatch.Job) (*time.Time, error) { timeRaw := job.Annotations[scheduledTimeAnnotation] if len(timeRaw) == 0 { return nil, nil } timeParsed, err := time.Parse(time.RFC3339, timeRaw) if err != nil { return nil, err } return &timeParsed, nil } // +kubebuilder:docs-gen:collapse=getScheduledTimeForJob for i, job := range childJobs.Items { _, finishedType := isJobFinished(&job) switch finishedType { case "": // ongoing activeJobs = append(activeJobs, &childJobs.Items[i]) case kbatch.JobFailed: failedJobs = append(failedJobs, &childJobs.Items[i]) case kbatch.JobComplete: successfulJobs = append(successfulJobs, &childJobs.Items[i]) } // We'll store the launch time in an annotation, so we'll reconstitute that from // the active jobs themselves. scheduledTimeForJob, err := getScheduledTimeForJob(&job) if err != nil { log.Error(err, "unable to parse schedule time for child job", "job", &job) continue } if scheduledTimeForJob != nil { if mostRecentTime == nil || mostRecentTime.Before(*scheduledTimeForJob) { mostRecentTime = scheduledTimeForJob } } } if mostRecentTime != nil { cronJob.Status.LastScheduleTime = &metav1.Time{Time: *mostRecentTime} } else { cronJob.Status.LastScheduleTime = nil } cronJob.Status.Active = nil for _, activeJob := range activeJobs { jobRef, err := ref.GetReference(r.Scheme, activeJob) if err != nil { log.Error(err, "unable to make reference to active job", "job", activeJob) continue } cronJob.Status.Active = append(cronJob.Status.Active, *jobRef) } /* Here, we'll log how many jobs we observed at a slightly higher logging level, for debugging. Notice how instead of using a format string, we use a fixed message, and attach key-value pairs with the extra information. This makes it easier to filter and query log lines. */ log.V(1).Info("job count", "active jobs", len(activeJobs), "successful jobs", len(successfulJobs), "failed jobs", len(failedJobs)) // Check if CronJob is suspended isSuspended := cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend // Update status conditions based on current state if isSuspended { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionFalse, Reason: "Suspended", Message: "CronJob is suspended", }) } else if len(failedJobs) > 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "JobsFailed", Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)), }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionFalse, Reason: "JobsFailed", Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)), }) } else if len(activeJobs) > 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionTrue, Reason: "JobsActive", Message: fmt.Sprintf("%d job(s) are currently active", len(activeJobs)), }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionTrue, Reason: "JobsActive", Message: fmt.Sprintf("CronJob is progressing with %d active job(s)", len(activeJobs)), }) } else { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionTrue, Reason: "AllJobsCompleted", Message: "All jobs have completed successfully", }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionFalse, Reason: "NoJobsActive", Message: "No jobs are currently active", }) } /* Using the data we've gathered, we'll update the status of our CRD. Just like before, we use our client. To specifically update the status subresource, we'll use the `Status` part of the client, with the `Update` method. The status subresource ignores changes to spec, so it's less likely to conflict with any other updates, and can have separate permissions. */ if err := r.Status().Update(ctx, &cronJob); err != nil { log.Error(err, "unable to update CronJob status") return ctrl.Result{}, err } /* Once we've updated our status, we can move on to ensuring that the status of the world matches what we want in our spec. ### 3: Clean up old jobs according to the history limit First, we'll try to clean up old jobs, so that we don't leave too many lying around. */ // NB: deleting these are "best effort" -- if we fail on a particular one, // we won't requeue just to finish the deleting. if cronJob.Spec.FailedJobsHistoryLimit != nil { slices.SortStableFunc(failedJobs, func(a, b *kbatch.Job) int { aStartTime := a.Status.StartTime bStartTime := b.Status.StartTime if aStartTime == nil && bStartTime != nil { return 1 } if aStartTime.Before(bStartTime) { return -1 } else if bStartTime.Before(aStartTime) { return 1 } return 0 }) for i, job := range failedJobs { if int32(i) >= int32(len(failedJobs))-*cronJob.Spec.FailedJobsHistoryLimit { break } if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { log.Error(err, "unable to delete old failed job", "job", job) } else { log.V(1).Info("deleted old failed job", "job", job) } } } if cronJob.Spec.SuccessfulJobsHistoryLimit != nil { slices.SortStableFunc(successfulJobs, func(a, b *kbatch.Job) int { aStartTime := a.Status.StartTime bStartTime := b.Status.StartTime if aStartTime == nil && bStartTime != nil { return 1 } if aStartTime.Before(bStartTime) { return -1 } else if bStartTime.Before(aStartTime) { return 1 } return 0 }) for i, job := range successfulJobs { if int32(i) >= int32(len(successfulJobs))-*cronJob.Spec.SuccessfulJobsHistoryLimit { break } if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil { log.Error(err, "unable to delete old successful job", "job", job) } else { log.V(1).Info("deleted old successful job", "job", job) } } } /* ### 4: Check if we're suspended If this object is suspended, we don't want to run any jobs, so we'll stop now. This is useful if something's broken with the job we're running and we want to pause runs to investigate or putz with the cluster, without deleting the object. */ if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend { log.V(1).Info("cronjob suspended, skipping") return ctrl.Result{}, nil } /* ### 5: Get the next scheduled run If we're not paused, we'll need to calculate the next scheduled run, and whether or not we've got a run that we haven't processed yet. */ /* We'll calculate the next scheduled time using our helpful cron library. We'll start calculating appropriate times from our last run, or the creation of the CronJob if we can't find a last run. If there are too many missed runs and we don't have any deadlines set, we'll bail so that we don't cause issues on controller restarts or wedges. Otherwise, we'll just return the missed runs (of which we'll just use the latest), and the next run, so that we can know when it's time to reconcile again. */ getNextSchedule := func(cronJob *batchv1.CronJob, now time.Time) (lastMissed time.Time, next time.Time, err error) { sched, err := cron.ParseStandard(cronJob.Spec.Schedule) if err != nil { return time.Time{}, time.Time{}, fmt.Errorf("unparseable schedule %q: %w", cronJob.Spec.Schedule, err) } // for optimization purposes, cheat a bit and start from our last observed run time // we could reconstitute this here, but there's not much point, since we've // just updated it. var earliestTime time.Time if cronJob.Status.LastScheduleTime != nil { earliestTime = cronJob.Status.LastScheduleTime.Time } else { earliestTime = cronJob.CreationTimestamp.Time } if cronJob.Spec.StartingDeadlineSeconds != nil { // controller is not going to schedule anything below this point schedulingDeadline := now.Add(-time.Second * time.Duration(*cronJob.Spec.StartingDeadlineSeconds)) if schedulingDeadline.After(earliestTime) { earliestTime = schedulingDeadline } } if earliestTime.After(now) { return time.Time{}, sched.Next(now), nil } starts := 0 for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) { lastMissed = t // An object might miss several starts. For example, if // controller gets wedged on Friday at 5:01pm when everyone has // gone home, and someone comes in on Tuesday AM and discovers // the problem and restarts the controller, then all the hourly // jobs, more than 80 of them for one hourly scheduledJob, should // all start running with no further intervention (if the scheduledJob // allows concurrency and late starts). // // However, if there is a bug somewhere, or incorrect clock // on controller's server or apiservers (for setting creationTimestamp) // then there could be so many missed start times (it could be off // by decades or more), that it would eat up all the CPU and memory // of this controller. In that case, we want to not try to list // all the missed start times. starts++ if starts > 100 { // We can't get the most recent times so just return an empty slice return time.Time{}, time.Time{}, fmt.Errorf("Too many missed start times (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.") //nolint:staticcheck } } return lastMissed, sched.Next(now), nil } // +kubebuilder:docs-gen:collapse=getNextSchedule // figure out the next times that we need to create // jobs at (or anything we missed). missedRun, nextRun, err := getNextSchedule(&cronJob, r.Now()) if err != nil { log.Error(err, "unable to figure out CronJob schedule") if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the schedule error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "InvalidSchedule", Message: fmt.Sprintf("Failed to parse schedule: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } // we don't really care about requeuing until we get an update that // fixes the schedule, so don't return an error return ctrl.Result{}, nil } /* We'll prep our eventual request to requeue until the next job, and then figure out if we actually need to run. */ scheduledResult := ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())} // save this so we can re-use it elsewhere log = log.WithValues("now", r.Now(), "next run", nextRun) /* ### 6: Run a new job if it's on schedule, not past the deadline, and not blocked by our concurrency policy If we've missed a run, and we're still within the deadline to start it, we'll need to run a job. */ if missedRun.IsZero() { log.V(1).Info("no upcoming scheduled times, sleeping until next") return scheduledResult, nil } // make sure we're not too late to start the run log = log.WithValues("current run", missedRun) tooLate := false if cronJob.Spec.StartingDeadlineSeconds != nil { tooLate = missedRun.Add(time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second).Before(r.Now()) } if tooLate { log.V(1).Info("missed starting deadline for last run, sleeping till next") if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect missed deadline meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "MissedSchedule", Message: fmt.Sprintf("Missed starting deadline for run at %v", missedRun), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return scheduledResult, nil } /* If we actually have to run a job, we'll need to either wait till existing ones finish, replace the existing ones, or just add new ones. If our information is out of date due to cache delay, we'll get a requeue when we get up-to-date information. */ // figure out how to run this job -- concurrency policy might forbid us from running // multiple at the same time... if cronJob.Spec.ConcurrencyPolicy == batchv1.ForbidConcurrent && len(activeJobs) > 0 { log.V(1).Info("concurrency policy blocks concurrent runs, skipping", "num active", len(activeJobs)) return scheduledResult, nil } // ...or instruct us to replace existing ones... if cronJob.Spec.ConcurrencyPolicy == batchv1.ReplaceConcurrent { for _, activeJob := range activeJobs { // we don't care if the job was already deleted if err := r.Delete(ctx, activeJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { log.Error(err, "unable to delete active job", "job", activeJob) return ctrl.Result{}, err } } } /* Once we've figured out what to do with existing jobs, we'll actually create our desired job */ /* We need to construct a job based on our CronJob's template. We'll copy over the spec from the template and copy some basic object meta. Then, we'll set the "scheduled time" annotation so that we can reconstitute our `LastScheduleTime` field each reconcile. Finally, we'll need to set an owner reference. This allows the Kubernetes garbage collector to clean up jobs when we delete the CronJob, and allows controller-runtime to figure out which cronjob needs to be reconciled when a given job changes (is added, deleted, completes, etc). */ constructJobForCronJob := func(cronJob *batchv1.CronJob, scheduledTime time.Time) (*kbatch.Job, error) { // We want job names for a given nominal start time to have a deterministic name to avoid the same job being created twice name := fmt.Sprintf("%s-%d", cronJob.Name, scheduledTime.Unix()) job := &kbatch.Job{ ObjectMeta: metav1.ObjectMeta{ Labels: make(map[string]string), Annotations: make(map[string]string), Name: name, Namespace: cronJob.Namespace, }, Spec: *cronJob.Spec.JobTemplate.Spec.DeepCopy(), } maps.Copy(job.Annotations, cronJob.Spec.JobTemplate.Annotations) job.Annotations[scheduledTimeAnnotation] = scheduledTime.Format(time.RFC3339) maps.Copy(job.Labels, cronJob.Spec.JobTemplate.Labels) if err := ctrl.SetControllerReference(cronJob, job, r.Scheme); err != nil { return nil, err } return job, nil } // +kubebuilder:docs-gen:collapse=constructJobForCronJob // actually make the job... job, err := constructJobForCronJob(&cronJob, missedRun) if err != nil { log.Error(err, "unable to construct job from template") // don't bother requeuing until we get a change to the spec return scheduledResult, nil } // ...and create it on the cluster if err := r.Create(ctx, job); err != nil { log.Error(err, "unable to create Job for CronJob", "job", job) if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "JobCreationFailed", Message: fmt.Sprintf("Failed to create job: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return ctrl.Result{}, err } log.V(1).Info("created Job for CronJob run", "job", job) if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect successful job creation meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionTrue, Reason: "JobCreated", Message: fmt.Sprintf("Created job %s", job.Name), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } /* ### 7: Requeue when we either see a running job or it's time for the next scheduled run Finally, we'll return the result that we prepped above, that says we want to requeue when our next run would need to occur. This is taken as a maximum deadline -- if something else changes in between, like our job starts or finishes, we get modified, etc, we might reconcile again sooner. */ // we'll requeue once we see the running job, and update our status return scheduledResult, nil } /* ### Setup Finally, we'll update our setup. In order to allow our reconciler to quickly look up Jobs by their owner, we'll need an index. We declare an index key that we can later use with the client as a pseudo-field name, and then describe how to extract the indexed value from the Job object. The indexer will automatically take care of namespaces for us, so we just have to extract the owner name if the Job has a CronJob owner. Additionally, we'll inform the manager that this controller owns some Jobs, so that it will automatically call Reconcile on the underlying CronJob when a Job changes, is deleted, etc. */ var ( jobOwnerKey = ".metadata.controller" apiGVStr = batchv1.GroupVersion.String() ) // SetupWithManager sets up the controller with the Manager. func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error { // set up a real clock, since we're not in a test if r.Clock == nil { r.Clock = realClock{} } if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kbatch.Job{}, jobOwnerKey, func(rawObj client.Object) []string { // grab the job object, extract the owner... job := rawObj.(*kbatch.Job) owner := metav1.GetControllerOf(job) if owner == nil { return nil } // ...make sure it's a CronJob... if owner.APIVersion != apiGVStr || owner.Kind != "CronJob" { return nil } // ...and if so, return it return []string{owner.Name} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). For(&batchv1.CronJob{}). Owns(&kbatch.Job{}). Named("cronjob"). Complete(r) } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/internal/controller/cronjob_controller_test.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // +kubebuilder:docs-gen:collapse=Apache License /* Ideally, we should have one `_controller_test.go` for each controller scaffolded and called in the `suite_test.go`. So, let's write our example test for the CronJob controller (`cronjob_controller_test.go.`) */ /* As usual, we start with the necessary imports. */ package controller import ( "context" "reflect" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" cronjobv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Imports /* The first step to writing a simple integration test is to actually create an instance of CronJob you can run tests against. Note that to create a CronJob, you'll need to create a stub CronJob struct that contains your CronJob's specifications. Note that when we create a stub CronJob, the CronJob also needs stubs of its required downstream objects. Without the stubbed Job template spec and the Pod template spec below, the Kubernetes API will not be able to create the CronJob. */ var _ = Describe("CronJob controller", func() { Context("CronJob controller test", func() { const CronjobName = "test-cronjob" ctx := context.Background() namespace := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: CronjobName, Namespace: CronjobName, }, } typeNamespacedName := types.NamespacedName{ Name: CronjobName, Namespace: CronjobName, } cronJob := &cronjobv1.CronJob{} SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) BeforeEach(func() { By("Creating the Namespace to perform the tests") err := k8sClient.Get(ctx, types.NamespacedName{Name: CronjobName}, &v1.Namespace{}) if err != nil && errors.IsNotFound(err) { err = k8sClient.Create(ctx, namespace) Expect(err).NotTo(HaveOccurred()) } By("creating the custom resource for the Kind CronJob") cronJob = &cronjobv1.CronJob{} err = k8sClient.Get(ctx, typeNamespacedName, cronJob) if err != nil && errors.IsNotFound(err) { /* Let's mock our custom resource the same way we would apply it from the manifest under config/samples */ cronJob = &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: CronjobName, Namespace: namespace.Name, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } err = k8sClient.Create(ctx, cronJob) Expect(err).NotTo(HaveOccurred()) } }) /* After each test, we clean up the resources created above. */ AfterEach(func() { By("removing the custom resource for the Kind CronJob") found := &cronjobv1.CronJob{} err := k8sClient.Get(ctx, typeNamespacedName, found) Expect(err).NotTo(HaveOccurred()) Eventually(func(g Gomega) { g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed()) }).Should(Succeed()) // TODO(user): Attention if you improve this code by adding other context test you MUST // be aware of the current delete namespace limitations. // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations By("Deleting the Namespace to perform the tests") _ = k8sClient.Delete(ctx, namespace) }) /* Now we can start implementing the test that validates the controller’s reconciliation behavior. */ It("should successfully reconcile a custom resource for CronJob", func() { By("Checking if the custom resource was successfully created") Eventually(func(g Gomega) { found := &cronjobv1.CronJob{} g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed()) }).Should(Succeed()) /* After creating this CronJob, let's verify that the controller properly initializes the status conditions. The controller runs in the background (started in suite_test.go), so it will automatically detect our CronJob and set initial conditions. */ By("Checking that status conditions are initialized") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Conditions).NotTo(BeEmpty()) }).Should(Succeed()) /* Now let's verify the CronJob has no active jobs initially. We use Gomega's `Consistently()` check here to ensure the status remains stable, confirming the controller isn't creating jobs prematurely. */ By("Checking that the CronJob has zero active Jobs") Consistently(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(BeEmpty()) }).WithTimeout(time.Second * 10).WithPolling(time.Millisecond * 250).Should(Succeed()) /* Next, we actually create a stubbed Job that will belong to our CronJob. We set the Job's status Active count to 2 to simulate the Job running two pods, which means the Job is actively running. We then set the Job's owner reference to point to our test CronJob. This ensures that the test Job belongs to, and is tracked by, our test CronJob. */ By("Creating a new Job owned by the CronJob") testJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "test-job", Namespace: namespace.Name, }, Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, } // Note that your CronJob’s GroupVersionKind is required to set up this owner reference. kind := reflect.TypeFor[cronjobv1.CronJob]().Name() gvk := cronjobv1.GroupVersion.WithKind(kind) controllerRef := metav1.NewControllerRef(cronJob, gvk) testJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) Expect(k8sClient.Create(ctx, testJob)).To(Succeed()) // Note that you can not manage the status values while creating the resource. // The status field is managed separately to reflect the current state of the resource. // Therefore, it should be updated using a PATCH or PUT operation after the resource has been created. // Additionally, it is recommended to use StatusConditions to manage the status. For further information see: // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status testJob.Status.Active = 2 Expect(k8sClient.Status().Update(ctx, testJob)).To(Succeed()) /* Adding this Job to our test CronJob should trigger our controller's reconciler logic. After that, we can verify whether our controller eventually updates our CronJob's Status field as expected! */ By("Checking that the CronJob has one active Job in status") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(HaveLen(1), "should have exactly one active job") g.Expect(cronJob.Status.Active[0].Name).To(Equal("test-job"), "the active job name should match") }).Should(Succeed()) /* Finally, let's verify that the controller properly set status conditions. Status conditions are a key part of Kubernetes API conventions and allow users and other controllers to understand the resource state. When there are active jobs, the Available condition should be True with reason JobsActive. */ By("Checking the latest Status Condition added to the CronJob instance") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var conditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &conditions)) Expect(conditions).To(HaveLen(1), "should have one Available condition") Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "Available should be True") Expect(conditions[0].Reason).To(Equal("JobsActive"), "reason should be JobsActive") }) }) }) /* After writing all this code, you can run `make test` or `go test ./...` in your `controllers/` directory again to run your new test! */ ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/internal/controller/suite_test.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* When we created the CronJob API with `kubebuilder create api` in a [previous chapter](/cronjob-tutorial/new-api.md), Kubebuilder already did some test work for you. Kubebuilder scaffolded a `internal/controller/suite_test.go` file that does the bare bones of setting up a test environment. First, it will contain the necessary imports. */ package controller import ( "context" "os" "path/filepath" "testing" "time" ctrl "sigs.k8s.io/controller-runtime" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" batchv1 "tutorial.kubebuilder.io/project/api/v1" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. // +kubebuilder:docs-gen:collapse=Imports /* Now, let's go through the code generated. */ var ( ctx context.Context cancel context.CancelFunc testEnv *envtest.Environment cfg *rest.Config k8sClient client.Client // You'll be using this client in your tests. ) func TestControllers(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Controller Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) var err error /* The CronJob Kind is added to the runtime scheme used by the test environment. This ensures that the CronJob API is registered with the scheme, allowing the test controller to recognize and interact with CronJob resources. */ err = batchv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) /* After the schemas, you will see the following marker. This marker is what allows new schemas to be added here automatically when a new API is added to the project. */ // +kubebuilder:scaffold:scheme /* The envtest environment is configured to load Custom Resource Definitions (CRDs) from the specified directory. This setup enables the test environment to recognize and interact with the custom resources defined by these CRDs. */ By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } // Retrieve the first found binary directory to allow running tests from IDEs if getFirstFoundEnvTestBinaryDir() != "" { testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() } /* Then, we start the envtest cluster. */ // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) /* A client is created for our test CRUD operations. */ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) /* One thing that this autogenerated file is missing, however, is a way to actually start your controller. The code above will set up a client for interacting with your custom Kind, but will not be able to test your controller behavior. If you want to test your custom controller logic, you’ll need to add some familiar-looking manager logic to your BeforeSuite() function, so you can register your custom controller to run on this test cluster. You may notice that the code below runs your controller with nearly identical logic to your CronJob project’s main.go! The only difference is that the manager is started in a separate goroutine so it does not block the cleanup of envtest when you’re done running your tests. Note that we set up both a "live" k8s client and a separate client from the manager. This is because when making assertions in tests, you generally want to assert against the live state of the API server. If you use the client from the manager (`k8sManager.GetClient`), you'd end up asserting against the contents of the cache instead, which is slower and can introduce flakiness into your tests. We could use the manager's `APIReader` to accomplish the same thing, but that would leave us with two clients in our test assertions and setup (one for reading, one for writing), and it'd be easy to make mistakes. Note that we keep the reconciler running against the manager's cache client, though -- we want our controller to behave as it would in production, and we use features of the cache (like indices) in our controller which aren't available when talking directly to the API server. */ k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, }) Expect(err).ToNot(HaveOccurred()) err = (&CronJobReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) Expect(err).ToNot(HaveOccurred(), "failed to run manager") }() }) /* Kubebuilder also generates boilerplate functions for cleaning up envtest and actually running your test files in your controllers/ directory. You won't need to touch these. */ var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) /* Now that you have your controller running on a test cluster and a client ready to perform operations on your CronJob, we can start writing integration tests! */ // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. // ENVTEST-based tests depend on specific binaries, usually located in paths set by // controller-runtime. When running tests directly (e.g., via an IDE) without using // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. // // This function streamlines the process by finding the required binaries, similar to // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are // properly set up, run 'make setup-envtest' beforehand. func getFirstFoundEnvTestBinaryDir() string { basePath := filepath.Join("..", "..", "bin", "k8s") entries, err := os.ReadDir(basePath) if err != nil { logf.Log.Error(err, "Failed to read directory", "path", basePath) return "" } for _, entry := range entries { if entry.IsDir() { return filepath.Join(basePath, entry.Name()) } } return "" } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package v1 import ( "context" "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Imports /* Next, we'll setup a logger for the webhooks. */ var cronjoblog = logf.Log.WithName("cronjob-resource") /* Then, we set up the webhook with the manager. */ // SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr, &batchv1.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, }). Complete() } /* Notice that we use kubebuilder markers to generate webhook manifests. This marker is responsible for generating a mutating webhook manifest. The meaning of each marker can be found [here](/reference/markers/webhook.md). */ /* This marker is responsible for generating a mutation webhook manifest. */ // +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob-v1.kb.io,admissionReviewVersions=v1 // CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind CronJob when those are created or updated. // // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as it is used only for temporary operations and does not need to be deeply copied. type CronJobCustomDefaulter struct { // Default values for various CronJob fields DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 } /* We use the `webhook.CustomDefaulter`interface to set defaults to our CRD. A webhook will automatically be served that calls this defaulting. The `Default`method is expected to mutate the receiver, setting the defaults. */ // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. func (d *CronJobCustomDefaulter) Default(_ context.Context, obj *batchv1.CronJob) error { cronjoblog.Info("Defaulting for CronJob", "name", obj.GetName()) // Set default values d.applyDefaults(obj) return nil } // applyDefaults applies default values to CronJob fields. func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) { if cronJob.Spec.ConcurrencyPolicy == "" { cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } if cronJob.Spec.Suspend == nil { cronJob.Spec.Suspend = new(bool) *cronJob.Spec.Suspend = d.DefaultSuspend } if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } if cronJob.Spec.FailedJobsHistoryLimit == nil { cronJob.Spec.FailedJobsHistoryLimit = new(int32) *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } /* We can validate our CRD beyond what's possible with declarative validation. Generally, declarative validation should be sufficient, but sometimes more advanced use cases call for complex validation. For instance, we'll see below that we use this to validate a well-formed cron schedule without making up a long regular expression. If `webhook.CustomValidator` interface is implemented, a webhook will automatically be served that calls the validation. The `ValidateCreate`, `ValidateUpdate` and `ValidateDelete` methods are expected to validate its receiver upon creation, update and deletion respectively. We separate out ValidateCreate from ValidateUpdate to allow behavior like making certain fields immutable, so that they can only be set on creation. ValidateDelete is also separated from ValidateUpdate to allow different validation behavior on deletion. Here, however, we just use the same shared validation for `ValidateCreate` and `ValidateUpdate`. And we do nothing in `ValidateDelete`, since we don't need to validate anything on deletion. */ /* This marker is responsible for generating a validation webhook manifest. */ // NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'. // +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1 // CronJobCustomValidator struct is responsible for validating the CronJob resource // when it is created, updated, or deleted. // // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as this struct is used only for temporary operations and does not need to be deeply copied. type CronJobCustomValidator struct { // TODO(user): Add more fields as needed for validation } // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon creation", "name", obj.GetName()) return nil, validateCronJob(obj) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *batchv1.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon update", "name", newObj.GetName()) return nil, validateCronJob(newObj) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon deletion", "name", obj.GetName()) // TODO(user): fill in your validation logic upon object deletion. return nil, nil } /* We validate the name and the spec of the CronJob. */ // validateCronJob validates the fields of a CronJob object. func validateCronJob(cronjob *batchv1.CronJob) error { var allErrs field.ErrorList if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil } return apierrors.NewInvalid( schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs) } /* Some fields are declaratively validated by OpenAPI schema. You can find kubebuilder validation markers (prefixed with `// +kubebuilder:validation`) in the [Designing an API](api-design.md) section. You can find all of the kubebuilder supported markers for declaring validation by running `controller-gen crd -w`, or [here](/reference/markers/crd-validation.md). */ func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error { // The field helpers from the kubernetes API machinery help us return nicely // structured validation errors. return validateScheduleFormat( cronjob.Spec.Schedule, field.NewPath("spec").Child("schedule")) } /* We'll need to validate the [cron](https://en.wikipedia.org/wiki/Cron) schedule is well-formatted. */ func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error { if _, err := cron.ParseStandard(schedule); err != nil { return field.Invalid(fldPath, schedule, err.Error()) } return nil } /* Validating the length of a string field can be done declaratively by the validation schema. But the `ObjectMeta.Name` field is defined in a shared package under the apimachinery repo, so we can't declaratively validate it using the validation schema. */ func validateCronJobName(cronjob *batchv1.CronJob) *field.Error { if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 { // The job name length is 63 characters like all Kubernetes objects // (which must fit in a DNS subdomain). The cronjob controller appends // a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating // a job. The job name length limit is 63 characters. Therefore cronjob // names must have length <= 63-11=52. If we don't validate this here, // then job creation will fail later. return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters") } return nil } // +kubebuilder:docs-gen:collapse=validateCronJobName() Code Implementation ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go ================================================ /* Copyright 2026 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 v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" batchv1 "tutorial.kubebuilder.io/project/api/v1" // TODO (user): Add any additional imports if needed "k8s.io/utils/ptr" ) var _ = Describe("CronJob Webhook", func() { var ( obj *batchv1.CronJob oldObj *batchv1.CronJob validator CronJobCustomValidator defaulter CronJobCustomDefaulter ) const validCronJobName = "valid-cronjob-name" const schedule = "*/5 * * * *" BeforeEach(func() { obj = &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: schedule, ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: ptr.To(int32(3)), FailedJobsHistoryLimit: ptr.To(int32(1)), }, } *obj.Spec.SuccessfulJobsHistoryLimit = 3 *obj.Spec.FailedJobsHistoryLimit = 1 oldObj = &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: schedule, ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: ptr.To(int32(3)), FailedJobsHistoryLimit: ptr.To(int32(1)), }, } *oldObj.Spec.SuccessfulJobsHistoryLimit = 3 *oldObj.Spec.FailedJobsHistoryLimit = 1 validator = CronJobCustomValidator{} defaulter = CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, } Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") }) AfterEach(func() { // TODO (user): Add any teardown logic common to all tests }) Context("When creating CronJob under Defaulting Webhook", func() { It("Should apply defaults when a required field is empty", func() { By("simulating a scenario where defaults should be applied") obj.Spec.ConcurrencyPolicy = "" // This should default to AllowConcurrent obj.Spec.Suspend = nil // This should default to false obj.Spec.SuccessfulJobsHistoryLimit = nil // This should default to 3 obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1 By("calling the Default method to apply defaults") _ = defaulter.Default(ctx, obj) By("checking that the default values are set") Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") Expect(*obj.Spec.Suspend).To(BeFalse(), "Expected Suspend to default to false") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(3)), "Expected SuccessfulJobsHistoryLimit to default to 3") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(1)), "Expected FailedJobsHistoryLimit to default to 1") }) It("Should not overwrite fields that are already set", func() { By("setting fields that would normally get a default") obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent obj.Spec.Suspend = ptr.To(true) obj.Spec.SuccessfulJobsHistoryLimit = ptr.To(int32(5)) obj.Spec.FailedJobsHistoryLimit = ptr.To(int32(2)) By("calling the Default method to apply defaults") _ = defaulter.Default(ctx, obj) By("checking that the fields were not overwritten") Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") Expect(obj.Spec.Suspend).NotTo(BeNil()) Expect(*obj.Spec.Suspend).To(BeTrue(), "Expected Suspend to retain its set value") Expect(obj.Spec.SuccessfulJobsHistoryLimit).NotTo(BeNil()) Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(5)), "Expected SuccessfulJobsHistoryLimit to retain its set value") Expect(obj.Spec.FailedJobsHistoryLimit).NotTo(BeNil()) Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(2)), "Expected FailedJobsHistoryLimit to retain its set value") }) }) Context("When creating or updating CronJob under Validating Webhook", func() { It("Should deny creation if the name is too long", func() { obj.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" Expect(validator.ValidateCreate(ctx, obj)).Error().To( MatchError(ContainSubstring("must be no more than 52 characters")), "Expected name validation to fail for a too-long name") }) It("Should admit creation if the name is valid", func() { obj.Name = validCronJobName Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), "Expected name validation to pass for a valid name") }) It("Should deny creation if the schedule is invalid", func() { obj.Spec.Schedule = "invalid-cron-schedule" Expect(validator.ValidateCreate(ctx, obj)).Error().To( MatchError(ContainSubstring("Expected exactly 5 fields, found 1: invalid-cron-schedule")), "Expected spec validation to fail for an invalid schedule") }) It("Should admit creation if the schedule is valid", func() { obj.Spec.Schedule = schedule Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), "Expected spec validation to pass for a valid schedule") }) It("Should deny update if both name and spec are invalid", func() { oldObj.Name = validCronJobName oldObj.Spec.Schedule = schedule By("simulating an update") obj.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" obj.Spec.Schedule = "invalid-cron-schedule" By("validating an update") Expect(validator.ValidateUpdate(ctx, oldObj, obj)).Error().To(HaveOccurred(), "Expected validation to fail for both name and spec") }) It("Should admit update if both name and spec are valid", func() { oldObj.Name = validCronJobName oldObj.Spec.Schedule = schedule By("simulating an update") obj.Name = "valid-cronjob-name-updated" obj.Spec.Schedule = "0 0 * * *" By("validating an update") Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil(), "Expected validation to pass for a valid update") }) }) }) ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go ================================================ /* Copyright 2026 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 v1 import ( "context" "crypto/tls" "fmt" "net" "os" "path/filepath" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" batchv1 "tutorial.kubebuilder.io/project/api/v1" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var ( ctx context.Context cancel context.CancelFunc k8sClient client.Client cfg *rest.Config testEnv *envtest.Environment ) func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) var err error err = batchv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: false, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, }, } // Retrieve the first found binary directory to allow running tests from IDEs if getFirstFoundEnvTestBinaryDir() != "" { testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() } // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) // start webhook server using Manager. webhookInstallOptions := &testEnv.WebhookInstallOptions mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, WebhookServer: webhook.NewServer(webhook.Options{ Host: webhookInstallOptions.LocalServingHost, Port: webhookInstallOptions.LocalServingPort, CertDir: webhookInstallOptions.LocalServingCertDir, }), LeaderElection: false, Metrics: metricsserver.Options{BindAddress: "0"}, }) Expect(err).NotTo(HaveOccurred()) err = SetupCronJobWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:webhook go func() { defer GinkgoRecover() err = mgr.Start(ctx) Expect(err).NotTo(HaveOccurred()) }() // wait for the webhook server to get ready. dialer := &net.Dialer{Timeout: time.Second} addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) Eventually(func() error { conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) if err != nil { return err } return conn.Close() }).Should(Succeed()) }) var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. // ENVTEST-based tests depend on specific binaries, usually located in paths set by // controller-runtime. When running tests directly (e.g., via an IDE) without using // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. // // This function streamlines the process by finding the required binaries, similar to // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are // properly set up, run 'make setup-envtest' beforehand. func getFirstFoundEnvTestBinaryDir() string { basePath := filepath.Join("..", "..", "..", "bin", "k8s") entries, err := os.ReadDir(basePath) if err != nil { logf.Log.Error(err, "Failed to read directory", "path", basePath) return "" } for _, entry := range entries { if entry.IsDir() { return filepath.Join(basePath, entry.Name()) } } return "" } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/test/e2e/e2e_suite_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2026 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 e2e import ( "fmt" "os" "os/exec" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "tutorial.kubebuilder.io/project/test/utils" ) var ( // managerImage is the manager image to be built and loaded for testing. managerImage = "example.com/project:v0.0.1" // shouldCleanupCertManager tracks whether CertManager was installed by this suite. shouldCleanupCertManager = false // shouldCleanupPrometheus tracks whether Prometheus was installed by this suite. shouldCleanupPrometheus = false ) // TestE2E runs the e2e test suite to validate the solution in an isolated environment. // The default setup requires Kind and CertManager. // // To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true func TestE2E(t *testing.T) { RegisterFailHandler(Fail) _, _ = fmt.Fprintf(GinkgoWriter, "Starting project e2e test suite\n") RunSpecs(t, "e2e suite") } var _ = BeforeSuite(func() { By("Ensure that Prometheus is enabled") _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") By("building the manager image") cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage)) _, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image") // TODO(user): If you want to change the e2e test vendor from Kind, // ensure the image is built and available, then remove the following block. By("loading the manager image on Kind") err = utils.LoadImageToKindClusterWithName(managerImage) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind") setupCertManager() By("checking if Prometheus is already installed") if !utils.IsPrometheusCRDsInstalled() { // Mark for cleanup before installation to handle interruptions and partial installs. shouldCleanupPrometheus = true By("installing Prometheus Operator") Expect(utils.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") } }) var _ = AfterSuite(func() { // Teardown Prometheus if it was installed by this suite if shouldCleanupPrometheus { By("uninstalling Prometheus Operator") utils.UninstallPrometheusOperator() } teardownCertManager() }) // setupCertManager installs CertManager if needed for webhook tests. // Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present. func setupCertManager() { if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" { _, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n") return } By("checking if CertManager is already installed") if utils.IsCertManagerCRDsInstalled() { _, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n") return } // Mark for cleanup before installation to handle interruptions and partial installs. shouldCleanupCertManager = true By("installing CertManager") Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") } // teardownCertManager uninstalls CertManager if it was installed by setupCertManager. // This ensures we only remove what we installed. func teardownCertManager() { if !shouldCleanupCertManager { _, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n") return } By("uninstalling CertManager") utils.UninstallCertManager() } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/test/e2e/e2e_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2026 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 e2e import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "tutorial.kubebuilder.io/project/test/utils" ) // namespace where the project is deployed in const namespace = "project-system" // serviceAccountName created for the project const serviceAccountName = "project-controller-manager" // metricsServiceName is the name of the metrics service of the project const metricsServiceName = "project-controller-manager-metrics-service" // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data const metricsRoleBindingName = "project-metrics-binding" var _ = Describe("Manager", Ordered, func() { var controllerPodName string // Before running the tests, set up the environment by creating the namespace, // enforce the restricted security policy to the namespace, installing CRDs, // and deploying the controller. BeforeAll(func() { By("creating manager namespace") cmd := exec.Command("kubectl", "create", "ns", namespace) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") By("labeling the namespace to enforce the restricted security policy") cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, "pod-security.kubernetes.io/enforce=restricted") _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") By("installing CRDs") cmd = exec.Command("make", "install") _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") By("deploying the controller-manager") cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") }) // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, // and deleting the namespace. AfterAll(func() { By("cleaning up the curl pod for metrics") cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) _, _ = utils.Run(cmd) By("undeploying the controller-manager") cmd = exec.Command("make", "undeploy") _, _ = utils.Run(cmd) By("uninstalling CRDs") cmd = exec.Command("make", "uninstall") _, _ = utils.Run(cmd) By("removing manager namespace") cmd = exec.Command("kubectl", "delete", "ns", namespace) _, _ = utils.Run(cmd) }) // After each test, check for failures and collect logs, events, // and pod descriptions for debugging. AfterEach(func() { specReport := CurrentSpecReport() if specReport.Failed() { By("Fetching controller manager pod logs") cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) controllerLogs, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) } By("Fetching Kubernetes events") cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") eventsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err) } By("Fetching curl-metrics logs") cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) metricsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err) } By("Fetching controller manager pod description") cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) podDescription, err := utils.Run(cmd) if err == nil { fmt.Println("Pod description:\n", podDescription) } else { fmt.Println("Failed to describe controller pod") } } }) SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) Context("Manager", func() { It("should run successfully", func() { By("validating that the controller-manager pod is running as expected") verifyControllerUp := func(g Gomega) { // Get the name of the controller-manager pod cmd := exec.Command("kubectl", "get", "pods", "-l", "control-plane=controller-manager", "-o", "go-template={{ range .items }}"+ "{{ if not .metadata.deletionTimestamp }}"+ "{{ .metadata.name }}"+ "{{ \"\\n\" }}{{ end }}{{ end }}", "-n", namespace, ) podOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") podNames := utils.GetNonEmptyLines(podOutput) g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") controllerPodName = podNames[0] g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) // Validate the pod's status cmd = exec.Command("kubectl", "get", "pods", controllerPodName, "-o", "jsonpath={.status.phase}", "-n", namespace, ) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") } Eventually(verifyControllerUp).Should(Succeed()) }) It("should ensure the metrics endpoint is serving metrics", func() { By("creating a ClusterRoleBinding for the service account to allow access to metrics") cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, "--clusterrole=project-metrics-reader", fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), ) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") By("validating that the metrics service is available") cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") By("validating that the ServiceMonitor for Prometheus is applied in the namespace") cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "ServiceMonitor should exist") By("getting the service account token") token, err := serviceAccountToken() Expect(err).NotTo(HaveOccurred()) Expect(token).NotTo(BeEmpty()) By("ensuring the controller pod is ready") verifyControllerPodReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace, "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("True"), "Controller pod not ready") } Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying that the controller manager is serving the metrics server") verifyMetricsServerStarted := func(g Gomega) { cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(ContainSubstring("Serving metrics server"), "Metrics server not yet started") } Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed()) By("waiting for the webhook service endpoints to be ready") verifyWebhookEndpointsReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "endpointslices.discovery.k8s.io", "-n", namespace, "-l", "kubernetes.io/service-name=project-webhook-service", "-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist") g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready") } Eventually(verifyWebhookEndpointsReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying the mutating webhook server is ready") verifyMutatingWebhookReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "mutatingwebhookconfigurations.admissionregistration.k8s.io", "project-mutating-webhook-configuration", "-o", "jsonpath={.webhooks[0].clientConfig.caBundle}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "MutatingWebhookConfiguration should exist") g.Expect(output).ShouldNot(BeEmpty(), "Mutating webhook CA bundle not yet injected") } Eventually(verifyMutatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying the validating webhook server is ready") verifyValidatingWebhookReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "validatingwebhookconfigurations.admissionregistration.k8s.io", "project-validating-webhook-configuration", "-o", "jsonpath={.webhooks[0].clientConfig.caBundle}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "ValidatingWebhookConfiguration should exist") g.Expect(output).ShouldNot(BeEmpty(), "Validating webhook CA bundle not yet injected") } Eventually(verifyValidatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed()) By("waiting additional time for webhook server to stabilize") time.Sleep(5 * time.Second) // +kubebuilder:scaffold:e2e-metrics-webhooks-readiness By("creating the curl-metrics pod to access the metrics endpoint") cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", "--namespace", namespace, "--image=curlimages/curl:latest", "--overrides", fmt.Sprintf(`{ "spec": { "containers": [{ "name": "curl", "image": "curlimages/curl:latest", "command": ["/bin/sh", "-c"], "args": [ "for i in $(seq 1 30); do curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics && exit 0 || sleep 2; done; exit 1" ], "securityContext": { "readOnlyRootFilesystem": true, "allowPrivilegeEscalation": false, "capabilities": { "drop": ["ALL"] }, "runAsNonRoot": true, "runAsUser": 1000, "seccompProfile": { "type": "RuntimeDefault" } } }], "serviceAccountName": "%s" } }`, token, metricsServiceName, namespace, serviceAccountName)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") By("waiting for the curl-metrics pod to complete.") verifyCurlUp := func(g Gomega) { cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", "-o", "jsonpath={.status.phase}", "-n", namespace) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") } Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) By("getting the metrics by checking curl-metrics logs") verifyMetricsAvailable := func(g Gomega) { metricsOutput, err := getMetricsOutput() g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") g.Expect(metricsOutput).NotTo(BeEmpty()) g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) } Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed()) }) It("should provisioned cert-manager", func() { By("validating that cert-manager has the certificate Secret") verifyCertManager := func(g Gomega) { cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) _, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) } Eventually(verifyCertManager).Should(Succeed()) }) It("should have CA injection for mutating webhooks", func() { By("checking CA injection for mutating webhooks") verifyCAInjection := func(g Gomega) { cmd := exec.Command("kubectl", "get", "mutatingwebhookconfigurations.admissionregistration.k8s.io", "project-mutating-webhook-configuration", "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") mwhOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(len(mwhOutput)).To(BeNumerically(">", 10)) } Eventually(verifyCAInjection).Should(Succeed()) }) It("should have CA injection for validating webhooks", func() { By("checking CA injection for validating webhooks") verifyCAInjection := func(g Gomega) { cmd := exec.Command("kubectl", "get", "validatingwebhookconfigurations.admissionregistration.k8s.io", "project-validating-webhook-configuration", "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") vwhOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) } Eventually(verifyCAInjection).Should(Succeed()) }) // +kubebuilder:scaffold:e2e-webhooks-checks // TODO: Customize the e2e test suite with scenarios specific to your project. // Consider applying sample/CR(s) and check their status and/or verifying // the reconciliation by using the metrics, i.e.: // metricsOutput, err := getMetricsOutput() // Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") // Expect(metricsOutput).To(ContainSubstring( // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, // strings.ToLower(), // )) }) }) // serviceAccountToken returns a token for the specified service account in the given namespace. // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request // and parsing the resulting token from the API response. func serviceAccountToken() (string, error) { const tokenRequestRawString = `{ "apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest" }` // Temporary file to store the token request secretName := fmt.Sprintf("%s-token-request", serviceAccountName) tokenRequestFile := filepath.Join("/tmp", secretName) err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) if err != nil { return "", err } var out string verifyTokenCreation := func(g Gomega) { // Execute kubectl command to create the token cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( "/api/v1/namespaces/%s/serviceaccounts/%s/token", namespace, serviceAccountName, ), "-f", tokenRequestFile) output, err := cmd.CombinedOutput() g.Expect(err).NotTo(HaveOccurred()) // Parse the JSON output to extract the token var token tokenRequest err = json.Unmarshal(output, &token) g.Expect(err).NotTo(HaveOccurred()) out = token.Status.Token } Eventually(verifyTokenCreation).Should(Succeed()) return out, err } // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. func getMetricsOutput() (string, error) { By("getting the curl-metrics logs") cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) return utils.Run(cmd) } // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, // containing only the token field that we need to extract. type tokenRequest struct { Status struct { Token string `json:"token"` } `json:"status"` } ================================================ FILE: docs/book/src/cronjob-tutorial/testdata/project/test/utils/utils.go ================================================ /* Copyright 2026 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 utils import ( "bufio" "bytes" "fmt" "os" "os/exec" "strings" . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck ) const ( certmanagerVersion = "v1.20.0" certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" defaultKindBinary = "kind" defaultKindCluster = "kind" prometheusOperatorVersion = "v0.89.0" prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + "releases/download/%s/bundle.yaml" ) func warnError(err error) { _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) } // Run executes the provided command within this context func Run(cmd *exec.Cmd) (string, error) { dir, _ := GetProjectDir() cmd.Dir = dir if err := os.Chdir(cmd.Dir); err != nil { _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err) } cmd.Env = append(os.Environ(), "GO111MODULE=on") command := strings.Join(cmd.Args, " ") _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command) output, err := cmd.CombinedOutput() if err != nil { return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err) } return string(output), nil } // UninstallCertManager uninstalls the cert manager func UninstallCertManager() { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } // Delete leftover leases in kube-system (not cleaned by default) kubeSystemLeases := []string{ "cert-manager-cainjector-leader-election", "cert-manager-controller", } for _, lease := range kubeSystemLeases { cmd = exec.Command("kubectl", "delete", "lease", lease, "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0") if _, err := Run(cmd); err != nil { warnError(err) } } } // InstallCertManager installs the cert manager bundle. func InstallCertManager() error { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "apply", "-f", url) if _, err := Run(cmd); err != nil { return err } // Wait for cert-manager-webhook to be ready, which can take time if cert-manager // was re-installed after uninstalling on a cluster. cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", "--for", "condition=Available", "--namespace", "cert-manager", "--timeout", "5m", ) _, err := Run(cmd) return err } // IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed // by verifying the existence of key CRDs related to Cert Manager. func IsCertManagerCRDsInstalled() bool { // List of common Cert Manager CRDs certManagerCRDs := []string{ "certificates.cert-manager.io", "issuers.cert-manager.io", "clusterissuers.cert-manager.io", "certificaterequests.cert-manager.io", "orders.acme.cert-manager.io", "challenges.acme.cert-manager.io", } // Execute the kubectl command to get all CRDs cmd := exec.Command("kubectl", "get", "crds") output, err := Run(cmd) if err != nil { return false } // Check if any of the Cert Manager CRDs are present crdList := GetNonEmptyLines(output) for _, crd := range certManagerCRDs { for _, line := range crdList { if strings.Contains(line, crd) { return true } } } return false } // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. func InstallPrometheusOperator() error { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "create", "-f", url) _, err := Run(cmd) return err } // UninstallPrometheusOperator uninstalls the prometheus func UninstallPrometheusOperator() { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } } // IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed // by verifying the existence of key CRDs related to Prometheus. func IsPrometheusCRDsInstalled() bool { // List of common Prometheus CRDs prometheusCRDs := []string{ "prometheuses.monitoring.coreos.com", "prometheusrules.monitoring.coreos.com", "prometheusagents.monitoring.coreos.com", } cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name") output, err := Run(cmd) if err != nil { return false } crdList := GetNonEmptyLines(output) for _, crd := range prometheusCRDs { for _, line := range crdList { if strings.Contains(line, crd) { return true } } } return false } // LoadImageToKindClusterWithName loads a local docker image to the kind cluster func LoadImageToKindClusterWithName(name string) error { cluster := defaultKindCluster if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { cluster = v } kindOptions := []string{"load", "docker-image", name, "--name", cluster} kindBinary := defaultKindBinary if v, ok := os.LookupEnv("KIND"); ok { kindBinary = v } cmd := exec.Command(kindBinary, kindOptions...) _, err := Run(cmd) return err } // GetNonEmptyLines converts given command output string into individual objects // according to line breakers, and ignores the empty elements in it. func GetNonEmptyLines(output string) []string { var res []string elements := strings.SplitSeq(output, "\n") for element := range elements { if element != "" { res = append(res, element) } } return res } // GetProjectDir will return the directory where the project is func GetProjectDir() (string, error) { wd, err := os.Getwd() if err != nil { return wd, fmt.Errorf("failed to get current working directory: %w", err) } wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } // UncommentCode searches for target in the file and remove the comment prefix // of the target content. The target content may span multiple lines. func UncommentCode(filename, target, prefix string) error { // false positive // nolint:gosec content, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } strContent := string(content) idx := strings.Index(strContent, target) if idx < 0 { return fmt.Errorf("unable to find the code %q to be uncommented", target) } out := new(bytes.Buffer) _, err = out.Write(content[:idx]) if err != nil { return fmt.Errorf("failed to write to output: %w", err) } scanner := bufio.NewScanner(bytes.NewBufferString(target)) if !scanner.Scan() { return nil } for { if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil { return fmt.Errorf("failed to write to output: %w", err) } // Avoid writing a newline in case the previous line was the last in target. if !scanner.Scan() { break } if _, err = out.WriteString("\n"); err != nil { return fmt.Errorf("failed to write to output: %w", err) } } if _, err = out.Write(content[idx+len(target):]); err != nil { return fmt.Errorf("failed to write to output: %w", err) } // false positive // nolint:gosec if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil { return fmt.Errorf("failed to write file %q: %w", filename, err) } return nil } ================================================ FILE: docs/book/src/cronjob-tutorial/webhook-implementation.md ================================================ # Implementing defaulting/validating webhooks If you want to implement [admission webhooks](../reference/admission-webhook.md) for your CRD, the only thing you need to do is to implement the `CustomDefaulter` and (or) the `CustomValidator` interface. Kubebuilder takes care of the rest for you, such as 1. Creating the webhook server. 1. Ensuring the server has been added in the manager. 1. Creating handlers for your webhooks. 1. Registering each handler with a path in your server. First, let's scaffold the webhooks for our CRD (CronJob). We'll need to run the following command with the `--defaulting` and `--programmatic-validation` flags (since our test project will use defaulting and validating webhooks): ```bash kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation ``` This will scaffold the webhook functions and register your webhook with the manager in your `main.go` for you. ## Custom Webhook Paths You can specify custom HTTP paths for your webhooks using the `--defaulting-path` and `--validation-path` flags: ```bash # Custom path for defaulting webhook kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --defaulting-path=/my-custom-mutate-path # Custom path for validation webhook kubebuilder create webhook --group batch --version v1 --kind CronJob --programmatic-validation --validation-path=/my-custom-validate-path # Both webhooks with different custom paths kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation \ --defaulting-path=/custom-mutate --validation-path=/custom-validate ``` This changes the path in the webhook marker annotation but does not change where the webhook files are scaffolded. The webhook files will still be created in `internal/webhook/v1/`. {{#literatego ./testdata/project/internal/webhook/v1/cronjob_webhook.go}} ================================================ FILE: docs/book/src/cronjob-tutorial/writing-tests.md ================================================ # Writing controller tests Testing Kubernetes controllers is a big subject, and the boilerplate testing files generated for you by kubebuilder are fairly minimal. To walk you through integration testing patterns for Kubebuilder-generated controllers, we will revisit the CronJob we built in our first tutorial and write a simple test for it. The basic approach is that, in your generated `suite_test.go` file, you will use envtest to create a local Kubernetes API server, instantiate and run your controllers, and then write additional `*_test.go` files to test it using [Ginkgo](http://onsi.github.io/ginkgo). If you want to tinker with how your envtest cluster is configured, see section [Configuring envtest for integration tests](../reference/envtest.md) as well as the [`envtest docs`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/envtest?tab=doc). ## Test Environment Setup {{#literatego ../cronjob-tutorial/testdata/project/internal/controller/suite_test.go}} ## Testing your Controller's Behavior {{#literatego ../cronjob-tutorial/testdata/project/internal/controller/cronjob_controller_test.go}} This Status update example above demonstrates a general testing strategy for a custom Kind with downstream objects. By this point, you hopefully have learned the following methods for testing your controller behavior: * Setting up your controller to run on an envtest cluster * Writing stubs for creating test objects * Isolating changes to an object to test specific controller behavior ================================================ FILE: docs/book/src/faq.md ================================================ # FAQ ## How does the value informed via the domain flag (i.e. `kubebuilder init --domain example.com`) when we init a project? After creating a project, usually you will want to extend the Kubernetes APIs and define new APIs which will be owned by your project. Therefore, the domain value is tracked in the [PROJECT][project-file-def] file which defines the config of your project and will be used as a domain to create the endpoints of your API(s). Please, ensure that you understand the [Groups and Versions and Kinds, oh my!][gvk]. The domain is for the group suffix, to explicitly show the resource group category. For example, if set `--domain=example.com`: ``` kubebuilder init --domain example.com --repo xxx --plugins=go/v4 kubebuilder create api --group mygroup --version v1beta1 --kind Mykind ``` Then the result resource group will be `mygroup.example.com`. > If domain field not set, the default value is `my.domain`. ## I'd like to customize my project to use [klog][klog] instead of the [zap][zap] provided by controller-runtime. How to use `klog` or other loggers as the project logger? In the `main.go` you can replace: ```go opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) ``` with: ```go flag.Parse() ctrl.SetLogger(klog.NewKlogr()) ``` ## After `make run`, I see errors like "unable to find leader election namespace: not running in-cluster..." You can enable the leader election. However, if you are testing the project locally using the `make run` target which will run the manager outside of the cluster then, you might also need to set the namespace the leader election resource will be created, as follows: ```go mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, Port: 9443, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "14be1926.testproject.org", LeaderElectionNamespace: "-system", ``` If you are running the project on the cluster with `make deploy` target then, you might not want to add this option. So, you might want to customize this behaviour using environment variables to only add this option for development purposes, such as: ```go leaderElectionNS := "" if os.Getenv("ENABLE_LEADER_ELECTION_NAMESPACE") != "false" { leaderElectionNS = "-system" } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, Port: 9443, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionNamespace: leaderElectionNS, LeaderElectionID: "14be1926.testproject.org", ... ``` ## I am facing the error "open /var/run/secrets/kubernetes.io/serviceaccount/token: permission denied" when I deploy my project against Kubernetes old versions. How to sort it out? If you are facing the error: ``` 1.6656687258729894e+09 ERROR controller-runtime.client.config unable to get kubeconfig {"error": "open /var/run/secrets/kubernetes.io/serviceaccount/token: permission denied"} sigs.k8s.io/controller-runtime/pkg/client/config.GetConfigOrDie /go/pkg/mod/sigs.k8s.io/controller-runtime@v0.13.0/pkg/client/config/config.go:153 main.main /workspace/main.go:68 runtime.main /usr/local/go/src/runtime/proc.go:250 ``` when you are running the project against a Kubernetes old version (maybe <= 1.21) , it might be caused by the [issue][permission-issue] , the reason is the mounted token file set to `0600`, see [solution][permission-PR] here. Then, the workaround is: Add `fsGroup` in the manager.yaml ```yaml securityContext: runAsNonRoot: true fsGroup: 65532 # add this fsGroup to make the token file readable ``` However, note that this problem is fixed and will not occur if you deploy the project in high versions (maybe >= 1.22). ## The error `Too long: must have at most 262144 bytes` is faced when I run `make install` to apply the CRD manifests. How to solve it? Why this error is faced? When attempting to run `make install` to apply the CRD manifests, the error `Too long: must have at most 262144 bytes may be encountered.` This error arises due to a size limit enforced by the Kubernetes API. Note that the `make install` target will apply the CRD manifest under `config/crd` using `kubectl apply -f -`. Therefore, when the apply command is used, the API annotates the object with the `last-applied-configuration` which contains the entire previous configuration. If this configuration is too large, it will exceed the allowed byte size. ([More info][k8s-obj-creation]) In ideal approach might use client-side apply might seem like the perfect solution since with the entire object configuration doesn't have to be stored as an annotation (last-applied-configuration) on the server. However, it's worth noting that as of now, it isn't supported by controller-gen or kubebuilder. For more on this, refer to: [Controller-tool-discussion][controller-tool-pr]. Therefore, you have a few options to workround this scenario such as: **By removing the descriptions from CRDs:** Your CRDs are generated using [controller-gen][controller-gen]. By using the option `maxDescLen=0` to remove the description, you may reduce the size, potentially resolving the issue. To do it you can update the Makefile as the following example and then, call the target `make manifest` to regenerate your CRDs without description, see: ```shell .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. # Note that the option maxDescLen=0 was added in the default scaffold in order to sort out the issue # Too long: must have at most 262144 bytes. By using kubectl apply to create / update resources an annotation # is created by K8s API to store the latest version of the resource ( kubectl.kubernetes.io/last-applied-configuration). # However, it has a size limit and if the CRD is too big with so many long descriptions as this one it will cause the failure. $(CONTROLLER_GEN) rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases ``` **By re-design your APIs:** You can review the design of your APIs and see if it has not more specs than should be by hurting single responsibility principle for example. So that you might to re-design them. ## How can I validate and parse fields in CRDs effectively? To enhance user experience, it is recommended to use [OpenAPI v3 schema](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.0.md#schemaObject) validation when writing your CRDs. However, this approach can sometimes require an additional parsing step. For example, consider this code ```go type StructName struct { // +kubebuilder:validation:Format=date-time TimeField string `json:"timeField,omitempty"` } ``` ### What happens in this scenario? - Users will receive an error notification from the Kubernetes API if they attempt to create a CRD with an invalid timeField value. - On the developer side, the string value needs to be parsed manually before use. ### Is there a better approach? To provide both a better user experience and a streamlined developer experience, it is advisable to use predefined types like [`metav1.Time`](https://pkg.go.dev/k8s.io/apimachinery@v0.31.1/pkg/apis/meta/v1#Time) For example, consider this code ```go type StructName struct { TimeField metav1.Time `json:"timeField,omitempty"` } ``` ### What happens in this scenario? - Users still receive error notifications from the Kubernetes API for invalid `timeField` values. - Developers can directly use the parsed TimeField in their code without additional parsing, reducing errors and improving efficiency. [k8s-obj-creation]: https://kubernetes.io/docs/tasks/manage-kubernetes-objects/declarative-config/#how-to-create-objects [gvk]: ./cronjob-tutorial/gvks.md [project-file-def]: ./reference/project-config.md [klog]: https://github.com/kubernetes/klog [zap]: https://github.com/uber-go/zap [permission-issue]: https://github.com/kubernetes/kubernetes/issues/82573 [permission-PR]: https://github.com/kubernetes/kubernetes/pull/89193 [controller-gen]: ./reference/controller-gen.html [controller-tool-pr]: https://github.com/kubernetes-sigs/controller-tools/pull/536 ================================================ FILE: docs/book/src/getting-started/testdata/project/.custom-gcl.yml ================================================ # This file configures golangci-lint with module plugins. # When you run 'make lint', it will automatically build a custom golangci-lint binary # with all the plugins listed below. # # See: https://golangci-lint.run/plugins/module-plugins/ version: v2.8.0 plugins: # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs) - module: "sigs.k8s.io/logtools" import: "sigs.k8s.io/logtools/logcheck/gclplugin" version: latest ================================================ FILE: docs/book/src/getting-started/testdata/project/.devcontainer/devcontainer.json ================================================ { "name": "Kubebuilder DevContainer", "image": "golang:1.25", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": { "moby": false, "dockerDefaultAddressPool": "base=172.30.0.0/16,size=24" }, "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/common-utils:2": { "upgradePackages": true } }, "runArgs": ["--privileged", "--init"], "customizations": { "vscode": { "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, "extensions": [ "ms-kubernetes-tools.vscode-kubernetes-tools", "ms-azuretools.vscode-docker" ] } }, "remoteEnv": { "GO111MODULE": "on" }, "onCreateCommand": "bash .devcontainer/post-install.sh" } ================================================ FILE: docs/book/src/getting-started/testdata/project/.devcontainer/post-install.sh ================================================ #!/bin/bash set -euo pipefail echo "====================================" echo "Kubebuilder DevContainer Setup" echo "====================================" # Verify running as root (required for installing to /usr/local/bin and /etc) if [ "$(id -u)" -ne 0 ]; then echo "ERROR: This script must be run as root" exit 1 fi echo "" echo "Detecting system architecture..." # Detect architecture using uname MACHINE=$(uname -m) case "${MACHINE}" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; *) echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64" ARCH="amd64" ;; esac echo "Architecture: ${ARCH}" echo "" echo "------------------------------------" echo "Setting up bash completion..." echo "------------------------------------" BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions" # Enable bash-completion in root's .bashrc (devcontainer runs as root) if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc echo "Added bash-completion to .bashrc" fi echo "" echo "------------------------------------" echo "Installing development tools..." echo "------------------------------------" # Install kind if ! command -v kind &> /dev/null; then echo "Installing kind..." curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}" chmod +x /usr/local/bin/kind echo "kind installed successfully" fi # Generate kind bash completion if command -v kind &> /dev/null; then if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then echo "kind completion installed" else echo "WARNING: Failed to generate kind completion" fi fi # Install kubebuilder if ! command -v kubebuilder &> /dev/null; then echo "Installing kubebuilder..." curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}" chmod +x /usr/local/bin/kubebuilder echo "kubebuilder installed successfully" fi # Generate kubebuilder bash completion if command -v kubebuilder &> /dev/null; then if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then echo "kubebuilder completion installed" else echo "WARNING: Failed to generate kubebuilder completion" fi fi # Install kubectl if ! command -v kubectl &> /dev/null; then echo "Installing kubectl..." KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt) curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" chmod +x /usr/local/bin/kubectl echo "kubectl installed successfully" fi # Generate kubectl bash completion if command -v kubectl &> /dev/null; then if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then echo "kubectl completion installed" else echo "WARNING: Failed to generate kubectl completion" fi fi # Generate Docker bash completion if command -v docker &> /dev/null; then if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then echo "docker completion installed" else echo "WARNING: Failed to generate docker completion" fi fi echo "" echo "------------------------------------" echo "Configuring Docker environment..." echo "------------------------------------" # Wait for Docker to be ready echo "Waiting for Docker to be ready..." for i in {1..30}; do if docker info >/dev/null 2>&1; then echo "Docker is ready" break fi if [ "$i" -eq 30 ]; then echo "WARNING: Docker not ready after 30s" fi sleep 1 done # Create kind network (ignore if already exists) if ! docker network inspect kind >/dev/null 2>&1; then if docker network create kind >/dev/null 2>&1; then echo "Created kind network" else echo "WARNING: Failed to create kind network (may already exist)" fi fi echo "" echo "------------------------------------" echo "Verifying installations..." echo "------------------------------------" kind version kubebuilder version kubectl version --client docker --version go version echo "" echo "====================================" echo "DevContainer ready!" echo "====================================" echo "All development tools installed successfully." echo "You can now start building Kubernetes operators." ================================================ FILE: docs/book/src/getting-started/testdata/project/.dockerignore ================================================ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file # Ignore everything by default and re-include only needed files ** # Re-include Go source files (but not *_test.go) !**/*.go **/*_test.go # Re-include Go module files !go.mod !go.sum ================================================ FILE: docs/book/src/getting-started/testdata/project/.github/workflows/auto_update.yml ================================================ name: Auto Update # The 'kubebuilder alpha update' command requires write access to the repository to create a branch # with the update files and allow you to open a pull request using the link provided in the issue. # The branch created will be named in the format kubebuilder-update-from--to- by default. # To protect your codebase, please ensure that you have branch protection rules configured for your # main branches. This will guarantee that no one can bypass a review and push directly to a branch like 'main'. permissions: contents: write # Create and push the update branch issues: write # Create GitHub Issue with PR link on: workflow_dispatch: schedule: - cron: "0 0 * * 2" # Every Tuesday at 00:00 UTC jobs: auto-update: runs-on: ubuntu-latest env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Checkout the repository. steps: - name: Checkout repository uses: actions/checkout@v4 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 # Configure Git to create commits with the GitHub Actions bot. - name: Configure Git run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" # Set up Go environment. - name: Set up Go uses: actions/setup-go@v5 with: go-version: stable # Install Kubebuilder. - name: Install Kubebuilder run: | curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)" chmod +x kubebuilder sudo mv kubebuilder /usr/local/bin/ kubebuilder version # Run the Kubebuilder alpha update command. # More info: https://kubebuilder.io/reference/commands/alpha_update - name: Run kubebuilder alpha update # Executes the update command with specified flags. # --force: Completes the merge even if conflicts occur, leaving conflict markers. # --push: Automatically pushes the resulting output branch to the 'origin' remote. # --restore-path: Preserves specified paths (e.g., CI workflow files) when squashing. # --open-gh-issue: Creates a GitHub Issue with a link for opening a PR for review. # # WARNING: This workflow does not use GitHub Models AI summary by default. # To enable AI-generated summaries in GitHub issues, you need permissions to use GitHub Models. # If you have the required permissions, re-run: # kubebuilder edit --plugins="autoupdate/v1-alpha" --use-gh-models run: | kubebuilder alpha update \ --force \ --push \ --restore-path .github/workflows \ --open-gh-issue ================================================ FILE: docs/book/src/getting-started/testdata/project/.github/workflows/lint.yml ================================================ name: Lint on: push: pull_request: jobs: lint: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Check linter configuration run: make lint-config - name: Run linter run: make lint ================================================ FILE: docs/book/src/getting-started/testdata/project/.github/workflows/test-chart.yml ================================================ name: Test Chart on: push: pull_request: jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Prepare project run: | go mod tidy make docker-build IMG=controller:latest kind load docker-image controller:latest - name: Install Helm run: make install-helm - name: Lint Helm Chart run: | helm lint ./dist/chart # TODO: Uncomment if cert-manager is enabled # - name: Install cert-manager via Helm (wait for readiness) # run: | # helm repo add jetstack https://charts.jetstack.io # helm repo update # helm install cert-manager jetstack/cert-manager \ # --namespace cert-manager \ # --create-namespace \ # --set crds.enabled=true \ # --wait \ # --timeout 300s # TODO: Uncomment if Prometheus is enabled # - name: Install Prometheus Operator CRDs # run: | # helm repo add prometheus-community https://prometheus-community.github.io/helm-charts # helm repo update # helm install prometheus-crds prometheus-community/prometheus-operator-crds - name: Deploy manager via Helm run: | make helm-deploy IMG=project:v0.1.0 - name: Check Helm release status run: | make helm-status ================================================ FILE: docs/book/src/getting-started/testdata/project/.github/workflows/test-e2e.yml ================================================ name: E2E Tests on: push: pull_request: jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Running Test e2e run: | go mod tidy make test-e2e ================================================ FILE: docs/book/src/getting-started/testdata/project/.github/workflows/test.yml ================================================ name: Tests on: push: pull_request: jobs: test: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Running Tests run: | go mod tidy make test ================================================ FILE: docs/book/src/getting-started/testdata/project/.gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin/* Dockerfile.cross # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Go workspace file go.work # Kubernetes Generated files - skip generated files, except for vendored files !vendor/**/zz_generated.* # editor and IDE paraphernalia .idea .vscode *.swp *.swo *~ # Kubeconfig might contain secrets *.kubeconfig ================================================ FILE: docs/book/src/getting-started/testdata/project/.golangci.yml ================================================ version: "2" run: allow-parallel-runners: true linters: default: none enable: - copyloopvar - dupl - errcheck - ginkgolinter - goconst - gocyclo - govet - ineffassign - lll - modernize - misspell - nakedret - prealloc - revive - staticcheck - unconvert - unparam - unused - logcheck settings: custom: logcheck: type: "module" description: Checks Go logging calls for Kubernetes logging conventions. revive: rules: - name: comment-spacings - name: import-shadowing modernize: disable: - omitzero exclusions: generated: lax rules: - linters: - lll path: api/* - linters: - dupl - lll path: internal/* paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: docs/book/src/getting-started/testdata/project/AGENTS.md ================================================ # project - AI Agent Guide ## Project Structure **Single-group layout (default):** ``` cmd/main.go Manager entry (registers controllers/webhooks) api//*_types.go CRD schemas (+kubebuilder markers) api//zz_generated.* Auto-generated (DO NOT EDIT) internal/controller/* Reconciliation logic internal/webhook/* Validation/defaulting (if present) config/crd/bases/* Generated CRDs (DO NOT EDIT) config/rbac/role.yaml Generated RBAC (DO NOT EDIT) config/samples/* Example CRs (edit these) Makefile Build/test/deploy commands PROJECT Kubebuilder metadata Auto-generated (DO NOT EDIT) ``` **Multi-group layout** (for projects with multiple API groups): ``` api///*_types.go CRD schemas by group internal/controller//* Controllers by group internal/webhook///* Webhooks by group and version (if present) ``` Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`. **To convert to multi-group layout:** 1. Run: `kubebuilder edit --multigroup=true` 2. Move APIs: `mkdir -p api/ && mv api/ api//` 3. Move controllers: `mkdir -p internal/controller/ && mv internal/controller/*.go internal/controller//` 4. Move webhooks (if present): `mkdir -p internal/webhook/ && mv internal/webhook/ internal/webhook//` 5. Update import paths in all files 6. Fix `path` in `PROJECT` file for each resource 7. Update test suite CRD paths (add one more `..` to relative paths) ## Critical Rules ### Never Edit These (Auto-Generated) - `config/crd/bases/*.yaml` - from `make manifests` - `config/rbac/role.yaml` - from `make manifests` - `config/webhook/manifests.yaml` - from `make manifests` - `**/zz_generated.*.go` - from `make generate` - `PROJECT` - from `kubebuilder [OPTIONS]` ### Never Remove Scaffold Markers Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers. ### Keep Project Structure Do not move files around. The CLI expects files in specific locations. ### Always Use CLI Commands Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually. ### E2E Tests Require an Isolated Kind Cluster The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI). Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster). ## After Making Changes **After editing `*_types.go` or markers:** ``` make manifests # Regenerate CRDs/RBAC from markers make generate # Regenerate DeepCopy methods ``` **After editing `*.go` files:** ``` make lint-fix # Auto-fix code style make test # Run unit tests ``` ## CLI Commands Cheat Sheet ### Create API (your own types) ```bash kubebuilder create api --group --version --kind ``` ### Deploy Image Plugin (scaffold to deploy/manage ANY container image) Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.): ```bash # Example: deploying memcached kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \ --image=memcached:alpine \ --plugins=deploy-image.go.kubebuilder.io/v1-alpha ``` Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation. ### Create Webhooks ```bash # Validation + defaulting kubebuilder create webhook --group --version --kind \ --defaulting --programmatic-validation # Conversion webhook (for multi-version APIs) kubebuilder create webhook --group --version v1 --kind \ --conversion --spoke v2 ``` ### Controller for Core Kubernetes Types ```bash # Watch Pods kubebuilder create api --group core --version v1 --kind Pod \ --controller=true --resource=false # Watch Deployments kubebuilder create api --group apps --version v1 --kind Deployment \ --controller=true --resource=false ``` ### Controller for External Types (e.g., from other operators) Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.): ```bash # Example: watching cert-manager Certificate resources kubebuilder create api \ --group cert-manager --version v1 --kind Certificate \ --controller=true --resource=false \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=io \ --external-api-module=github.com/cert-manager/cert-manager ``` **Note:** Use `--external-api-module=@` only if you need a specific version. Otherwise, omit `@` to use what's in go.mod. ### Webhook for External Types ```bash # Example: validating external resources kubebuilder create webhook \ --group cert-manager --version v1 --kind Issuer \ --defaulting \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=io \ --external-api-module=github.com/cert-manager/cert-manager ``` ## Testing & Development ```bash make test # Run unit tests (uses envtest: real K8s API + etcd) make run # Run locally (uses current kubeconfig context) ``` Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup. ## Deployment Workflow ```bash # 1. Regenerate manifests make manifests generate # 2. Build & deploy export IMG=/:tag make docker-build docker-push IMG=$IMG # Or: kind load docker-image $IMG --name make deploy IMG=$IMG # 3. Test kubectl apply -k config/samples/ # 4. Debug kubectl logs -n -system deployment/-controller-manager -c manager -f ``` ### API Design **Key markers for** `api//*_types.go`: ```go // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Namespaced // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status" // On fields: // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:Pattern="^[a-z]+$" // +kubebuilder:default="value" ``` - **Use** `metav1.Condition` for status (not custom string fields) - **Use predefined types**: `metav1.Time` instead of `string` for dates - **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`) ### Controller Design **RBAC markers in** `internal/controller/*_controller.go`: ```go // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete ``` **Implementation rules:** - **Idempotent reconciliation**: Safe to run multiple times - **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts - **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)` - **Owner references**: Enable automatic garbage collection (`SetControllerReference`) - **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter` - **Finalizers**: Clean up external resources (buckets, VMs, DNS entries) ### Logging **Follow Kubernetes logging message style guidelines:** - Start from a capital letter - Do not end the message with a period - Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`) - Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"` - Specify object type: `"Deleted Pod"` not `"Deleted"` - Balanced key-value pairs ```go log.Info("Starting reconciliation") log.Info("Created Deployment", "name", deploy.Name) log.Error(err, "Failed to create Pod", "name", name) ``` **Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines ### Webhooks - **Create all types together**: `--defaulting --programmatic-validation --conversion` - **When`--force`is used**: Backup custom logic first, then restore after scaffolding - **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`) - Hub version: Usually oldest stable version (v1) - Spoke versions: Newer versions that convert to/from hub (v2, v3) - Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke) ### Learning from Examples The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation: ```bash kubebuilder create api --group example --version v1alpha1 --kind MyApp \ --image= --plugins=deploy-image.go.kubebuilder.io/v1-alpha ``` Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation. ## Distribution Options ### Option 1: YAML Bundle (Kustomize) ```bash # Generate dist/install.yaml from Kustomize manifests make build-installer IMG=/:tag ``` **Key points:** - The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment) - Commit this file to your repository for easy distribution - Users only need `kubectl` to install (no additional tools required) **Example:** Users install with a single command: ```bash kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml ``` ### Option 2: Helm Chart ```bash kubebuilder edit --plugins=helm/v2-alpha # Generates dist/chart/ (default) kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts # Generates charts/chart/ ``` **For development:** ```bash make helm-deploy IMG=/: # Deploy manager via Helm make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..." # Deploy with custom values make helm-status # Show release status make helm-uninstall # Remove release make helm-history # View release history make helm-rollback # Rollback to previous version ``` **For end users/production:** ```bash helm install my-release .//chart/ --namespace --create-namespace ``` **Important:** If you add webhooks or modify manifests after initial chart generation: 1. Backup any customizations in `/chart/values.yaml` and `/chart/manager/manager.yaml` 2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized) 3. Manually restore your custom values from the backup ### Publish Container Image ```bash export IMG=/: make docker-build docker-push IMG=$IMG ``` ## References ### Essential Reading - **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide) - **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions) - **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.) - **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels) ### API Design & Implementation - **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md - **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ - **Markers Reference**: https://book.kubebuilder.io/reference/markers.html ### Tools & Libraries - **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime - **controller-tools**: https://github.com/kubernetes-sigs/controller-tools - **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder ================================================ FILE: docs/book/src/getting-started/testdata/project/Dockerfile ================================================ # Build the manager binary FROM golang:1.25 AS builder ARG TARGETOS ARG TARGETARCH WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the Go source (relies on .dockerignore to filter) COPY . . # Build # the GOARCH has no default value to allow the binary to be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 ENTRYPOINT ["/manager"] ================================================ FILE: docs/book/src/getting-started/testdata/project/Makefile ================================================ # Image URL to use all building/pushing image targets IMG ?= controller:latest # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif # CONTAINER_TOOL defines the container tool to be used for building images. # Be aware that the target commands are only tested with Docker which is # scaffolded by default. However, you might want to replace it to use other # tools. (i.e. podman) CONTAINER_TOOL ?= docker # Setting SHELL to bash allows bash commands to be executed by recipes. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec .PHONY: all all: build ##@ General # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the # target descriptions by '##'. The awk command is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. # More info on the usage of ANSI control characters for terminal formatting: # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters # More info on the awk command: # http://linuxcommand.org/lc3_adv_awk.php .PHONY: help help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ##@ Development .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. go fmt ./... .PHONY: vet vet: ## Run go vet against code. go vet ./... .PHONY: test test: manifests generate fmt vet setup-envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. # CertManager is installed by default; skip with: # - CERT_MANAGER_INSTALL_SKIP=true KIND_CLUSTER ?= project-test-e2e .PHONY: setup-test-e2e setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist @command -v $(KIND) >/dev/null 2>&1 || { \ echo "Kind is not installed. Please install Kind manually."; \ exit 1; \ } @case "$$($(KIND) get clusters)" in \ *"$(KIND_CLUSTER)"*) \ echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ *) \ echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ esac .PHONY: test-e2e test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v $(MAKE) cleanup-test-e2e .PHONY: cleanup-test-e2e cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests @$(KIND) delete cluster --name $(KIND_CLUSTER) .PHONY: lint lint: golangci-lint ## Run golangci-lint linter "$(GOLANGCI_LINT)" run .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes "$(GOLANGCI_LINT)" run --fix .PHONY: lint-config lint-config: golangci-lint ## Verify golangci-lint linter configuration "$(GOLANGCI_LINT)" config verify ##@ Build .PHONY: build build: manifests generate fmt vet ## Build manager binary. go build -o bin/manager cmd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. go run ./cmd/main.go # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build docker-build: ## Build docker image with the manager. $(CONTAINER_TOOL) build -t ${IMG} . .PHONY: docker-push docker-push: ## Push docker image with the manager. $(CONTAINER_TOOL) push ${IMG} # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le .PHONY: docker-buildx docker-buildx: ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - $(CONTAINER_TOOL) buildx create --name project-builder $(CONTAINER_TOOL) buildx use project-builder - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - $(CONTAINER_TOOL) buildx rm project-builder rm Dockerfile.cross .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} "$(KUSTOMIZE)" build config/default > dist/install.yaml ##@ Deployment ifndef ignore-not-found ignore-not-found = false endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f - ##@ Dependencies ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p "$(LOCALBIN)" ## Tool Binaries KUBECTL ?= kubectl KIND ?= kind KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions KUSTOMIZE_VERSION ?= v5.8.1 CONTROLLER_TOOLS_VERSION ?= v0.20.1 #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \ [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \ printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \ [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \ printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/') GOLANGCI_LINT_VERSION ?= v2.8.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. $(KUSTOMIZE): $(LOCALBIN) $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. $(CONTROLLER_GEN): $(LOCALBIN) $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) .PHONY: setup-envtest setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." @"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \ echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ exit 1; \ } .PHONY: envtest envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. $(ENVTEST): $(LOCALBIN) $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) @test -f .custom-gcl.yml && { \ echo "Building custom golangci-lint with plugins..." && \ $(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \ mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \ } || true # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed # $3 - specific version of package define go-install-tool @[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ set -e; \ package=$(2)@$(3) ;\ echo "Downloading $${package}" ;\ rm -f "$(1)" ;\ GOBIN="$(LOCALBIN)" go install $${package} ;\ mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\ } ;\ ln -sf "$$(realpath "$(1)-$(3)")" "$(1)" endef define gomodver $(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null) endef ##@ Helm Deployment ## Helm binary to use for deploying the chart HELM ?= helm ## Namespace to deploy the Helm release HELM_NAMESPACE ?= project-system ## Name of the Helm release HELM_RELEASE ?= project ## Path to the Helm chart directory HELM_CHART_DIR ?= dist/chart ## Additional arguments to pass to helm commands HELM_EXTRA_ARGS ?= .PHONY: install-helm install-helm: ## Install the latest version of Helm. @command -v $(HELM) >/dev/null 2>&1 || { \ echo "Installing Helm..." && \ curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \ } .PHONY: helm-deploy helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG. $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \ --namespace $(HELM_NAMESPACE) \ --create-namespace \ --set manager.image.repository=$${IMG%:*} \ --set manager.image.tag=$${IMG##*:} \ --wait \ --timeout 5m \ $(HELM_EXTRA_ARGS) .PHONY: helm-uninstall helm-uninstall: ## Uninstall the Helm release from the K8s cluster. $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-status helm-status: ## Show Helm release status. $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-history helm-history: ## Show Helm release history. $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-rollback helm-rollback: ## Rollback to previous Helm release. $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) ================================================ FILE: docs/book/src/getting-started/testdata/project/PROJECT ================================================ # Code generated by tool. DO NOT EDIT. # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html cliVersion: (devel) domain: example.com layout: - go.kubebuilder.io/v4 plugins: autoupdate.kubebuilder.io/v1-alpha: {} helm.kubebuilder.io/v2-alpha: manifests: dist/install.yaml output: dist projectName: project repo: example.com/memcached resources: - api: crdVersion: v1 namespaced: true controller: true domain: example.com group: cache kind: Memcached path: example.com/memcached/api/v1alpha1 version: v1alpha1 version: "3" ================================================ FILE: docs/book/src/getting-started/testdata/project/README.md ================================================ # project // TODO(user): Add simple overview of use/purpose ## Description // TODO(user): An in-depth paragraph about your project and overview of use ## Getting Started ### Prerequisites - go version v1.24.6+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. ### To Deploy on the cluster **Build and push your image to the location specified by `IMG`:** ```sh make docker-build docker-push IMG=/project:tag ``` **NOTE:** This image ought to be published in the personal registry you specified. And it is required to have access to pull the image from the working environment. Make sure you have the proper permission to the registry if the above commands don’t work. **Install the CRDs into the cluster:** ```sh make install ``` **Deploy the Manager to the cluster with the image specified by `IMG`:** ```sh make deploy IMG=/project:tag ``` > **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin privileges or be logged in as admin. **Create instances of your solution** You can apply the samples (examples) from the config/sample: ```sh kubectl apply -k config/samples/ ``` >**NOTE**: Ensure that the samples has default values to test it out. ### To Uninstall **Delete the instances (CRs) from the cluster:** ```sh kubectl delete -k config/samples/ ``` **Delete the APIs(CRDs) from the cluster:** ```sh make uninstall ``` **UnDeploy the controller from the cluster:** ```sh make undeploy ``` ## Project Distribution Following the options to release and provide this solution to the users. ### By providing a bundle with all YAML files 1. Build the installer for the image built and published in the registry: ```sh make build-installer IMG=/project:tag ``` **NOTE:** The makefile target mentioned above generates an 'install.yaml' file in the dist directory. This file contains all the resources built with Kustomize, which are necessary to install this project without its dependencies. 2. Using the installer Users can just run 'kubectl apply -f ' to install the project, i.e.: ```sh kubectl apply -f https://raw.githubusercontent.com//project//dist/install.yaml ``` ### By providing a Helm Chart 1. Build the chart using the optional helm plugin ```sh kubebuilder edit --plugins=helm/v2-alpha ``` 2. See that a chart was generated under 'dist/chart', and users can obtain this solution from there. **NOTE:** If you change the project, you need to update the Helm Chart using the same command above to sync the latest changes. Furthermore, if you create webhooks, you need to use the above command with the '--force' flag and manually ensure that any custom configuration previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' is manually re-applied afterwards. ## Contributing // TODO(user): Add detailed information on how you would like others to contribute to this project **NOTE:** Run `make help` for more information on all potential `make` targets More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) ## License Copyright 2026 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: docs/book/src/getting-started/testdata/project/api/v1alpha1/groupversion_info.go ================================================ /* Copyright 2026 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 v1alpha1 contains API Schema definitions for the cache v1alpha1 API group. // +kubebuilder:object:generate=true // +groupName=cache.example.com package v1alpha1 import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) var ( // SchemeGroupVersion is group version used to register these objects. // This name is used by applyconfiguration generators (e.g. controller-gen). SchemeGroupVersion = schema.GroupVersion{Group: "cache.example.com", Version: "v1alpha1"} // GroupVersion is an alias for SchemeGroupVersion, for backward compatibility. GroupVersion = SchemeGroupVersion // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) ================================================ FILE: docs/book/src/getting-started/testdata/project/api/v1alpha1/memcached_types.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package v1alpha1 import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // +kubebuilder:docs-gen:collapse=Imports // MemcachedSpec defines the desired state of Memcached type MemcachedSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // The following markers will use OpenAPI v3 schema to validate the value // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html // size defines the number of Memcached instances // The following markers will use OpenAPI v3 schema to validate the value // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=3 // +kubebuilder:validation:ExclusiveMaximum=false // +optional Size *int32 `json:"size,omitempty"` } // MemcachedStatus defines the observed state of Memcached. type MemcachedStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // For Kubernetes API conventions, see: // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties // conditions represent the current state of the Memcached resource. // Each condition has a unique type and reflects the status of a specific aspect of the resource. // // Standard condition types include: // - "Available": the resource is fully functional // - "Progressing": the resource is being created or updated // - "Degraded": the resource failed to reach or maintain its desired state // // The status of each condition is one of True, False, or Unknown. // +listType=map // +listMapKey=type // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // Memcached is the Schema for the memcacheds API type Memcached struct { metav1.TypeMeta `json:",inline"` // metadata is a standard object metadata // +optional metav1.ObjectMeta `json:"metadata,omitzero"` // spec defines the desired state of Memcached // +required Spec MemcachedSpec `json:"spec"` // status defines the observed state of Memcached // +optional Status MemcachedStatus `json:"status,omitzero"` } // +kubebuilder:object:root=true // MemcachedList contains a list of Memcached type MemcachedList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitzero"` Items []Memcached `json:"items"` } func init() { SchemeBuilder.Register(&Memcached{}, &MemcachedList{}) } ================================================ FILE: docs/book/src/getting-started/testdata/project/api/v1alpha1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2026 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 controller-gen. DO NOT EDIT. package v1alpha1 import ( "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Memcached) DeepCopyInto(out *Memcached) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Memcached. func (in *Memcached) DeepCopy() *Memcached { if in == nil { return nil } out := new(Memcached) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *Memcached) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MemcachedList) DeepCopyInto(out *MemcachedList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]Memcached, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedList. func (in *MemcachedList) DeepCopy() *MemcachedList { if in == nil { return nil } out := new(MemcachedList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *MemcachedList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MemcachedSpec) DeepCopyInto(out *MemcachedSpec) { *out = *in if in.Size != nil { in, out := &in.Size, &out.Size *out = new(int32) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedSpec. func (in *MemcachedSpec) DeepCopy() *MemcachedSpec { if in == nil { return nil } out := new(MemcachedSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MemcachedStatus) DeepCopyInto(out *MemcachedStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedStatus. func (in *MemcachedStatus) DeepCopy() *MemcachedStatus { if in == nil { return nil } out := new(MemcachedStatus) in.DeepCopyInto(out) return out } ================================================ FILE: docs/book/src/getting-started/testdata/project/cmd/main.go ================================================ /* Copyright 2026 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 main import ( "crypto/tls" "flag" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" cachev1alpha1 "example.com/memcached/api/v1alpha1" "example.com/memcached/internal/controller" // +kubebuilder:scaffold:imports ) var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(cachev1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } // nolint:gocyclo func main() { var metricsAddr string var metricsCertPath, metricsCertName, metricsCertKey string var webhookCertPath, webhookCertName, webhookCertKey string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") flag.StringVar(&metricsCertPath, "metrics-cert-path", "", "The directory that contains the metrics server certificate.") flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and // Rapid Reset CVEs. For more information see: // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 // - https://github.com/advisories/GHSA-4374-p667-p6c8 disableHTTP2 := func(c *tls.Config) { setupLog.Info("Disabling HTTP/2") c.NextProtos = []string{"http/1.1"} } if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) } // Initial webhook TLS options webhookTLSOpts := tlsOpts webhookServerOptions := webhook.Options{ TLSOpts: webhookTLSOpts, } if len(webhookCertPath) > 0 { setupLog.Info("Initializing webhook certificate watcher using provided certificates", "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) webhookServerOptions.CertDir = webhookCertPath webhookServerOptions.CertName = webhookCertName webhookServerOptions.KeyName = webhookCertKey } webhookServer := webhook.NewServer(webhookServerOptions) // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, SecureServing: secureMetrics, TLSOpts: tlsOpts, } if secureMetrics { // FilterProvider is used to protect the metrics endpoint with authn/authz. // These configurations ensure that only authorized users and service accounts // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/filters#WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } // If the certificate is not specified, controller-runtime will automatically // generate self-signed certificates for the metrics server. While convenient for development and testing, // this setup is not recommended for production. // // TODO(user): If you enable certManager, uncomment the following lines: // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates // managed by cert-manager for the metrics server. // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. if len(metricsCertPath) > 0 { setupLog.Info("Initializing metrics certificate watcher using provided certificates", "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) metricsServerOptions.CertDir = metricsCertPath metricsServerOptions.CertName = metricsCertName metricsServerOptions.KeyName = metricsCertKey } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "4b13cc52.example.com", // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily // when the Manager ends. This requires the binary to immediately end when the // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly // speeds up voluntary leader transitions as the new leader don't have to wait // LeaseDuration time first. // // In the default scaffold provided, the program ends immediately after // the manager stops, so would be fine to enable this option. However, // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, }) if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) } if err := (&controller.MemcachedReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "Memcached") os.Exit(1) } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up ready check") os.Exit(1) } setupLog.Info("Starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "Failed to run manager") os.Exit(1) } } ================================================ FILE: docs/book/src/getting-started/testdata/project/config/crd/bases/cache.example.com_memcacheds.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.20.1 name: memcacheds.cache.example.com spec: group: cache.example.com names: kind: Memcached listKind: MemcachedList plural: memcacheds singular: memcached scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: Memcached is the Schema for the memcacheds API properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: spec defines the desired state of Memcached properties: size: description: |- size defines the number of Memcached instances The following markers will use OpenAPI v3 schema to validate the value More info: https://book.kubebuilder.io/reference/markers/crd-validation.html format: int32 maximum: 3 minimum: 1 type: integer type: object status: description: status defines the observed state of Memcached properties: conditions: description: |- conditions represent the current state of the Memcached resource. Each condition has a unique type and reflects the status of a specific aspect of the resource. Standard condition types include: - "Available": the resource is fully functional - "Progressing": the resource is being created or updated - "Degraded": the resource failed to reach or maintain its desired state The status of each condition is one of True, False, or Unknown. items: description: Condition contains details for one aspect of the current state of this API Resource. properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: |- message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: |- observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: |- reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map type: object required: - spec type: object served: true storage: true subresources: status: {} ================================================ FILE: docs/book/src/getting-started/testdata/project/config/crd/kustomization.yaml ================================================ # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: - bases/cache.example.com_memcacheds.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD # +kubebuilder:scaffold:crdkustomizewebhookpatch # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. #configurations: #- kustomizeconfig.yaml ================================================ FILE: docs/book/src/getting-started/testdata/project/config/crd/kustomizeconfig.yaml ================================================ # This file is for teaching kustomize how to substitute name and namespace reference in CRD nameReference: - kind: Service version: v1 fieldSpecs: - kind: CustomResourceDefinition version: v1 group: apiextensions.k8s.io path: spec/conversion/webhook/clientConfig/service/name varReference: - path: metadata/annotations ================================================ FILE: docs/book/src/getting-started/testdata/project/config/default/cert_metrics_manager_patch.yaml ================================================ # This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. # Add the volumeMount for the metrics-server certs - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true # Add the --metrics-cert-path argument for the metrics server - op: add path: /spec/template/spec/containers/0/args/- value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs # Add the metrics-server certs volume configuration - op: add path: /spec/template/spec/volumes/- value: name: metrics-certs secret: secretName: metrics-server-cert optional: false items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key ================================================ FILE: docs/book/src/getting-started/testdata/project/config/default/kustomization.yaml ================================================ # Adds namespace to all resources. namespace: project-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: project- # Labels to add to all resources and selectors. #labels: #- includeSelectors: true # pairs: # someName: someValue resources: - ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will # be able to communicate with the Webhook Server. #- ../network-policy # Uncomment the patches line if you enable Metrics patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: kind: Deployment # Uncomment the patches line if you enable Metrics and CertManager # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. # This patch will protect the metrics with certManager self-signed certs. #- path: cert_metrics_manager_patch.yaml # target: # kind: Deployment # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- path: manager_webhook_patch.yaml # target: # kind: Deployment # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations #replacements: # - source: # Uncomment the following block to enable certificates for metrics # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.name # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 0 # create: true # - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor # kind: ServiceMonitor # group: monitoring.coreos.com # version: v1 # name: controller-manager-metrics-monitor # fieldPaths: # - spec.endpoints.0.tlsConfig.serverName # options: # delimiter: '.' # index: 0 # create: true # - source: # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.namespace # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 1 # create: true # - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor # kind: ServiceMonitor # group: monitoring.coreos.com # version: v1 # name: controller-manager-metrics-monitor # fieldPaths: # - spec.endpoints.0.tlsConfig.serverName # options: # delimiter: '.' # index: 1 # create: true # - source: # Uncomment the following block if you have any webhook # kind: Service # version: v1 # name: webhook-service # fieldPath: .metadata.name # Name of the service # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPaths: # - .spec.dnsNames.0 # - .spec.dnsNames.1 # options: # delimiter: '.' # index: 0 # create: true # - source: # kind: Service # version: v1 # name: webhook-service # fieldPath: .metadata.namespace # Namespace of the service # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPaths: # - .spec.dnsNames.0 # - .spec.dnsNames.1 # options: # delimiter: '.' # index: 1 # create: true # - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # This name should match the one in certificate.yaml # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # - select: # kind: ValidatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # - select: # kind: ValidatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # - select: # kind: MutatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # - select: # kind: MutatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectionns # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectionname ================================================ FILE: docs/book/src/getting-started/testdata/project/config/default/manager_metrics_patch.yaml ================================================ # This patch adds the args to allow exposing the metrics endpoint using HTTPS - op: add path: /spec/template/spec/containers/0/args/0 value: --metrics-bind-address=:8443 ================================================ FILE: docs/book/src/getting-started/testdata/project/config/default/metrics_service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-service namespace: system spec: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 selector: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/getting-started/testdata/project/config/manager/kustomization.yaml ================================================ resources: - manager.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller newName: controller newTag: latest ================================================ FILE: docs/book/src/getting-started/testdata/project/config/manager/manager.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: system --- apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize spec: selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: control-plane: controller-manager app.kubernetes.io/name: project spec: # TODO(user): Uncomment the following code to configure the nodeAffinity expression # according to the platforms which are supported by your solution. # It is considered best practice to support multiple architectures. You can # build your manager image using the makefile target docker-buildx. # affinity: # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: kubernetes.io/arch # operator: In # values: # - amd64 # - arm64 # - ppc64le # - s390x # - key: kubernetes.io/os # operator: In # values: # - linux securityContext: # Projects are configured by default to adhere to the "restricted" Pod Security Standards. # This ensures that deployments meet the highest security requirements for Kubernetes. # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - command: - /manager args: - --leader-elect - --health-probe-bind-address=:8081 image: controller:latest name: manager ports: [] securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: - "ALL" livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 # TODO(user): Configure the resources accordingly based on the project requirements. # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi volumeMounts: [] volumes: [] serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 ================================================ FILE: docs/book/src/getting-started/testdata/project/config/network-policy/allow-metrics-traffic.yaml ================================================ # This NetworkPolicy allows ingress traffic # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those # namespaces are able to gather data from the metrics endpoint. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: allow-metrics-traffic namespace: system spec: podSelector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project policyTypes: - Ingress ingress: # This allows ingress traffic from any namespace with the label metrics: enabled - from: - namespaceSelector: matchLabels: metrics: enabled # Only from namespaces with this label ports: - port: 8443 protocol: TCP ================================================ FILE: docs/book/src/getting-started/testdata/project/config/network-policy/kustomization.yaml ================================================ resources: - allow-metrics-traffic.yaml ================================================ FILE: docs/book/src/getting-started/testdata/project/config/prometheus/kustomization.yaml ================================================ resources: - monitor.yaml # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus # to securely reference certificates created and managed by cert-manager. # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml # to mount the "metrics-server-cert" secret in the Manager Deployment. #patches: # - path: monitor_tls_patch.yaml # target: # kind: ServiceMonitor ================================================ FILE: docs/book/src/getting-started/testdata/project/config/prometheus/monitor.yaml ================================================ # Prometheus Monitor Service (Metrics) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system spec: endpoints: - path: /metrics port: https # Ensure this is the name of the port that exposes HTTPS metrics scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables # certificate verification, exposing the system to potential man-in-the-middle attacks. # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, # which securely references the certificate from the 'metrics-server-cert' secret. insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/getting-started/testdata/project/config/prometheus/monitor_tls_patch.yaml ================================================ # Patch for Prometheus ServiceMonitor to enable secure TLS configuration # using certificates managed by cert-manager - op: replace path: /spec/endpoints/0/tlsConfig value: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc insecureSkipVerify: false ca: secret: name: metrics-server-cert key: ca.crt cert: secret: name: metrics-server-cert key: tls.crt keySecret: name: metrics-server-cert key: tls.key ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/kustomization.yaml ================================================ resources: # All RBAC will be applied under this service account in # the deployment namespace. You may comment out this resource # if your manager will use a service account that exists at # runtime. Be sure to update RoleBinding and ClusterRoleBinding # subjects if changing service account names. - service_account.yaml - role.yaml - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # The following RBAC configurations are used to protect # the metrics endpoint with authn/authz. These configurations # ensure that only authorized users and service accounts # can access the metrics endpoint. Comment the following # permissions if you want to disable this protection. # More info: https://book.kubebuilder.io/reference/metrics.html - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by # default, aiding admins in cluster management. Those roles are # not used by the project itself. You can comment the following lines # if you do not want those helpers be installed with your Project. - memcached_admin_role.yaml - memcached_editor_role.yaml - memcached_viewer_role.yaml ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/leader_election_role.yaml ================================================ # permissions to do leader election. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/leader_election_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: leader-election-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/memcached_admin_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants full permissions ('*') over cache.example.com. # This role is intended for users authorized to modify roles and bindings within the cluster, # enabling them to delegate specific permissions to other users or groups as needed. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: memcached-admin-role rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - '*' - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/memcached_editor_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants permissions to create, update, and delete resources within the cache.example.com. # This role is intended for users who need to manage these resources # but should not control RBAC or manage permissions for others. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: memcached-editor-role rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/memcached_viewer_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants read-only access to cache.example.com resources. # This role is intended for users who need visibility into these resources # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: memcached-viewer-role rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - get - list - watch - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/metrics_auth_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-auth-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/metrics_auth_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: metrics-auth-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: metrics-auth-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/metrics_reader_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-reader rules: - nonResourceURLs: - "/metrics" verbs: - get ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/role.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: manager-role rules: - apiGroups: - "" resources: - pods verbs: - get - list - watch - apiGroups: - apps resources: - deployments verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds/finalizers verbs: - update - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get - patch - update - apiGroups: - events.k8s.io resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: manager-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/getting-started/testdata/project/config/rbac/service_account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager namespace: system ================================================ FILE: docs/book/src/getting-started/testdata/project/config/samples/cache_v1alpha1_memcached.yaml ================================================ apiVersion: cache.example.com/v1alpha1 kind: Memcached metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: memcached-sample spec: # TODO(user): edit the following value to ensure the number # of Pods/Instances your Operand must have on cluster size: 1 ================================================ FILE: docs/book/src/getting-started/testdata/project/config/samples/kustomization.yaml ================================================ ## Append samples of your project ## resources: - cache_v1alpha1_memcached.yaml # +kubebuilder:scaffold:manifestskustomizesamples ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/.helmignore ================================================ # Patterns to ignore when building Helm packages. # Operating system files .DS_Store # Version control directories .git/ .gitignore .bzr/ .hg/ .hgignore .svn/ # Backup and temporary files *.swp *.tmp *.bak *.orig *~ # IDE and editor-related files .idea/ .vscode/ # Helm chart artifacts dist/chart/*.tgz ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/Chart.yaml ================================================ apiVersion: v2 name: project description: A Helm chart to distribute project type: application version: 0.1.0 appVersion: "0.1.0" keywords: - kubernetes - operator annotations: kubebuilder.io/generated-by: kubebuilder ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/NOTES.txt ================================================ Thank you for installing {{ .Chart.Name }}. Your release is named {{ .Release.Name }}. The controller and CRDs have been installed in namespace {{ .Release.Namespace }}. To verify the installation: kubectl get pods -n {{ .Release.Namespace }} kubectl get customresourcedefinitions To learn more about the release, try: $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "project.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "project.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Namespace for generated references. Always uses the Helm release namespace. */}} {{- define "project.namespaceName" -}} {{- .Release.Namespace }} {{- end }} {{/* Resource name with proper truncation for Kubernetes 63-character limit. Takes a dict with: - .suffix: Resource name suffix (e.g., "metrics", "webhook") - .context: Template context (root context with .Values, .Release, etc.) Dynamically calculates safe truncation to ensure total name length <= 63 chars. */}} {{- define "project.resourceName" -}} {{- $fullname := include "project.fullname" .context }} {{- $suffix := .suffix }} {{- $maxLen := sub 62 (len $suffix) | int }} {{- if gt (len $fullname) $maxLen }} {{- printf "%s-%s" (trunc $maxLen $fullname | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" $fullname $suffix | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/crd/memcacheds.cache.example.com.yaml ================================================ {{- if .Values.crd.enable }} apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: {{- if .Values.crd.keep }} "helm.sh/resource-policy": keep {{- end }} controller-gen.kubebuilder.io/version: v0.20.1 name: memcacheds.cache.example.com spec: group: cache.example.com names: kind: Memcached listKind: MemcachedList plural: memcacheds singular: memcached scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: Memcached is the Schema for the memcacheds API properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: spec defines the desired state of Memcached properties: size: description: |- size defines the number of Memcached instances The following markers will use OpenAPI v3 schema to validate the value More info: https://book.kubebuilder.io/reference/markers/crd-validation.html format: int32 maximum: 3 minimum: 1 type: integer type: object status: description: status defines the observed state of Memcached properties: conditions: description: |- conditions represent the current state of the Memcached resource. Each condition has a unique type and reflects the status of a specific aspect of the resource. Standard condition types include: - "Available": the resource is fully functional - "Progressing": the resource is being created or updated - "Degraded": the resource failed to reach or maintain its desired state The status of each condition is one of True, False, or Unknown. items: description: Condition contains details for one aspect of the current state of this API Resource. properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: |- message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: |- observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: |- reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map type: object required: - spec type: object served: true storage: true subresources: status: {} {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/manager/manager.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} spec: replicas: {{ .Values.manager.replicas }} selector: matchLabels: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} control-plane: controller-manager spec: {{- with .Values.manager.tolerations }} tolerations: {{ toYaml . | nindent 10 }} {{- end }} {{- with .Values.manager.affinity }} affinity: {{ toYaml . | nindent 10 }} {{- end }} {{- with .Values.manager.nodeSelector }} nodeSelector: {{ toYaml . | nindent 10 }} {{- end }} containers: - args: {{- if .Values.metrics.enable }} - --metrics-bind-address=:{{ .Values.metrics.port }} {{- else }} # Bind to :0 to disable the controller-runtime managed metrics server - --metrics-bind-address=0 {{- end }} - --health-probe-bind-address=:8081 {{- range .Values.manager.args }} - {{ . }} {{- end }} command: - /manager image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}" imagePullPolicy: {{ .Values.manager.image.pullPolicy }} livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 name: manager ports: [] readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 resources: {{- if .Values.manager.resources }} {{- toYaml .Values.manager.resources | nindent 10 }} {{- else }} {} {{- end }} securityContext: {{- if .Values.manager.securityContext }} {{- toYaml .Values.manager.securityContext | nindent 10 }} {{- else }} {} {{- end }} volumeMounts: [] securityContext: {{- if .Values.manager.podSecurityContext }} {{- toYaml .Values.manager.podSecurityContext | nindent 8 }} {{- else }} {} {{- end }} serviceAccountName: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} terminationGracePeriodSeconds: 10 volumes: [] ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }} namespace: {{ .Release.Namespace }} spec: ports: - name: https port: {{ .Values.metrics.port }} protocol: TCP targetPort: {{ .Values.metrics.port }} selector: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/monitoring/servicemonitor.yaml ================================================ {{- if .Values.prometheus.enable }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-monitor" "context" $) }} namespace: {{ .Release.Namespace }} spec: endpoints: - path: /metrics port: https scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: {{- if .Values.certManager.enable }} serverName: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc # Apply secure TLS configuration with cert-manager insecureSkipVerify: false ca: secret: name: metrics-server-cert key: ca.crt cert: secret: name: metrics-server-cert key: tls.crt keySecret: name: metrics-server-cert key: tls.key {{- else }} # Development/Test mode (insecure configuration) insecureSkipVerify: true {{- end }} selector: matchLabels: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/controller-manager.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/leader-election-role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "leader-election-role" "context" $) }} namespace: {{ .Release.Namespace }} rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/leader-election-rolebinding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "leader-election-rolebinding" "context" $) }} namespace: {{ .Release.Namespace }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ include "project.resourceName" (dict "suffix" "leader-election-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/manager-role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "manager-role" "context" $) }} rules: - apiGroups: - "" resources: - pods verbs: - get - list - watch - apiGroups: - apps resources: - deployments verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds/finalizers verbs: - update - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get - patch - update - apiGroups: - events.k8s.io resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/manager-rolebinding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "manager-rolebinding" "context" $) }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ include "project.resourceName" (dict "suffix" "manager-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/memcached-admin-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "memcached-admin-role" "context" $) }} rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - '*' - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/memcached-editor-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "memcached-editor-role" "context" $) }} rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/memcached-viewer-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "memcached-viewer-role" "context" $) }} rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - get - list - watch - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/metrics-auth-role.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/metrics-auth-rolebinding.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-rolebinding" "context" $) }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/templates/rbac/metrics-reader.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-reader" "context" $) }} rules: - nonResourceURLs: - /metrics verbs: - get {{- end }} ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/chart/values.yaml ================================================ ## String to partially override chart.fullname template (will maintain the release name) ## # nameOverride: "" ## String to fully override chart.fullname template ## # fullnameOverride: "" ## Configure the controller manager deployment ## manager: replicas: 1 image: repository: controller tag: latest pullPolicy: IfNotPresent ## Arguments ## args: - --leader-elect ## Environment variables ## env: [] ## Env overrides (--set manager.envOverrides.VAR=value) ## Same name in env above: this value takes precedence. ## envOverrides: {} ## Image pull secrets ## imagePullSecrets: [] ## Pod-level security settings ## podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault ## Container-level security settings ## securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true ## Resource limits and requests ## resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi ## Manager pod's affinity ## affinity: {} ## Manager pod's node selector ## nodeSelector: {} ## Manager pod's tolerations ## tolerations: [] ## Helper RBAC roles for managing custom resources ## rbacHelpers: # Install convenience admin/editor/viewer roles for CRDs enable: false ## Custom Resource Definitions ## crd: # Install CRDs with the chart enable: true # Keep CRDs when uninstalling keep: true ## Controller metrics endpoint. ## Enable to expose /metrics endpoint with RBAC protection. ## metrics: enable: true # Metrics server port port: 8443 ## Cert-manager integration for TLS certificates. ## Required for webhook certificates and metrics endpoint certificates. ## certManager: enable: false ## Prometheus ServiceMonitor for metrics scraping. ## Requires prometheus-operator to be installed in the cluster. ## prometheus: enable: false ================================================ FILE: docs/book/src/getting-started/testdata/project/dist/install.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-system --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.20.1 name: memcacheds.cache.example.com spec: group: cache.example.com names: kind: Memcached listKind: MemcachedList plural: memcacheds singular: memcached scope: Namespaced versions: - name: v1alpha1 schema: openAPIV3Schema: description: Memcached is the Schema for the memcacheds API properties: apiVersion: description: |- APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources type: string kind: description: |- Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds type: string metadata: type: object spec: description: spec defines the desired state of Memcached properties: size: description: |- size defines the number of Memcached instances The following markers will use OpenAPI v3 schema to validate the value More info: https://book.kubebuilder.io/reference/markers/crd-validation.html format: int32 maximum: 3 minimum: 1 type: integer type: object status: description: status defines the observed state of Memcached properties: conditions: description: |- conditions represent the current state of the Memcached resource. Each condition has a unique type and reflects the status of a specific aspect of the resource. Standard condition types include: - "Available": the resource is fully functional - "Progressing": the resource is being created or updated - "Degraded": the resource failed to reach or maintain its desired state The status of each condition is one of True, False, or Unknown. items: description: Condition contains details for one aspect of the current state of this API Resource. properties: lastTransitionTime: description: |- lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. format: date-time type: string message: description: |- message is a human readable message indicating details about the transition. This may be an empty string. maxLength: 32768 type: string observedGeneration: description: |- observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. format: int64 minimum: 0 type: integer reason: description: |- reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: description: status of the condition, one of True, False, Unknown. enum: - "True" - "False" - Unknown type: string type: description: type of condition in CamelCase or in foo.example.com/CamelCase. maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map type: object required: - spec type: object served: true storage: true subresources: status: {} --- apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-leader-election-role namespace: project-system rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-manager-role rules: - apiGroups: - "" resources: - pods verbs: - get - list - watch - apiGroups: - apps resources: - deployments verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds/finalizers verbs: - update - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get - patch - update - apiGroups: - events.k8s.io resources: - events verbs: - create - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-memcached-admin-role rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - '*' - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-memcached-editor-role rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-memcached-viewer-role rules: - apiGroups: - cache.example.com resources: - memcacheds verbs: - get - list - watch - apiGroups: - cache.example.com resources: - memcacheds/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-metrics-auth-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-metrics-reader rules: - nonResourceURLs: - /metrics verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-leader-election-rolebinding namespace: project-system roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: project-leader-election-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: project-manager-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: project-metrics-auth-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: project-metrics-auth-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager-metrics-service namespace: project-system spec: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 selector: app.kubernetes.io/name: project control-plane: controller-manager --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager namespace: project-system spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: project control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: app.kubernetes.io/name: project control-plane: controller-manager spec: containers: - args: - --metrics-bind-address=:8443 - --leader-elect - --health-probe-bind-address=:8081 command: - /manager image: controller:latest livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 name: manager ports: [] readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true volumeMounts: [] securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault serviceAccountName: project-controller-manager terminationGracePeriodSeconds: 10 volumes: [] ================================================ FILE: docs/book/src/getting-started/testdata/project/go.mod ================================================ module example.com/memcached go 1.25.3 require ( github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 k8s.io/api v0.35.0 k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.23.3 ) require ( cel.dev/expr v0.24.0 // indirect github.com/Masterminds/semver/v3 v3.4.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.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // 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-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.3 // 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/pprof v0.0.0-20250403155104-27863c87afa6 // 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.0 // 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.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.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.29.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.38.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.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.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.0 // 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/kube-openapi v0.0.0-20250910181357-589584f1c912 // 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.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: docs/book/src/getting-started/testdata/project/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.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 v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 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.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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.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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-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/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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 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.1/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/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.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/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/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.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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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/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/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/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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 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-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.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/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 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/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= 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.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: docs/book/src/getting-started/testdata/project/hack/boilerplate.go.txt ================================================ /* Copyright 2026 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: docs/book/src/getting-started/testdata/project/internal/controller/memcached_controller.go ================================================ /* Copyright 2026 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 controller import ( "context" "fmt" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" 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/types" "k8s.io/utils/ptr" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" cachev1alpha1 "example.com/memcached/api/v1alpha1" ) // Definitions to manage status conditions const ( // typeAvailableMemcached represents the status of the Deployment reconciliation typeAvailableMemcached = "Available" ) // MemcachedReconciler reconciles a Memcached object type MemcachedReconciler struct { client.Client Scheme *runtime.Scheme } // +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // It is essential for the controller's reconciliation loop to be idempotent. By following the Operator // pattern you will create Controllers which provide a reconcile function // responsible for synchronizing resources until the desired state is reached on the cluster. // Breaking this recommendation goes against the design principles of controller-runtime. // and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention. // For further info: // - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ // - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/ // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logf.FromContext(ctx) // Fetch the Memcached instance // The purpose is check if the Custom Resource for the Kind Memcached // is applied on the cluster if not we return nil to stop the reconciliation memcached := &cachev1alpha1.Memcached{} err := r.Get(ctx, req.NamespacedName, memcached) if err != nil { if apierrors.IsNotFound(err) { // If the custom resource is not found then it usually means that it was deleted or not created // In this way, we will stop the reconciliation log.Info("Memcached resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get memcached") return ctrl.Result{}, err } // Let's just set the status as Unknown when no status is available if len(memcached.Status.Conditions) == 0 { meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) if err = r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } // Let's re-fetch the memcached Custom Resource after updating the status // so that we have the latest state of the resource on the cluster and we will avoid // raising the error "the object has been modified, please apply // your changes to the latest version and try again" which would re-trigger the reconciliation // if we try to update it again in the following operations if err := r.Get(ctx, req.NamespacedName, memcached); err != nil { log.Error(err, "Failed to re-fetch memcached") return ctrl.Result{}, err } } // Check if the deployment already exists, if not create a new one found := &appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found) if err != nil && apierrors.IsNotFound(err) { // Define a new deployment dep, err := r.deploymentForMemcached(memcached) if err != nil { log.Error(err, "Failed to define new Deployment resource for Memcached") // The following implementation will update the status meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionFalse, Reason: "Reconciling", Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", memcached.Name, err)}) if err := r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } return ctrl.Result{}, err } log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) if err = r.Create(ctx, dep); err != nil { log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) return ctrl.Result{}, err } // Deployment created successfully // We will requeue the reconciliation so that we can ensure the state // and move forward for the next operations return ctrl.Result{RequeueAfter: time.Minute}, nil } else if err != nil { log.Error(err, "Failed to get Deployment") // Let's return the error for the reconciliation be re-trigged again return ctrl.Result{}, err } // If the size is not defined in the Custom Resource then we will set the desired replicas to 0 var desiredReplicas int32 = 0 if memcached.Spec.Size != nil { desiredReplicas = *memcached.Spec.Size } // The CRD API defines that the Memcached type have a MemcachedSpec.Size field // to set the quantity of Deployment instances to the desired state on the cluster. // Therefore, the following code will ensure the Deployment size is the same as defined // via the Size spec of the Custom Resource which we are reconciling. if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas { found.Spec.Replicas = ptr.To(desiredReplicas) if err = r.Update(ctx, found); err != nil { log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) // Re-fetch the memcached Custom Resource before updating the status // so that we have the latest state of the resource on the cluster and we will avoid // raising the error "the object has been modified, please apply // your changes to the latest version and try again" which would re-trigger the reconciliation if err := r.Get(ctx, req.NamespacedName, memcached); err != nil { log.Error(err, "Failed to re-fetch memcached") return ctrl.Result{}, err } // The following implementation will update the status meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionFalse, Reason: "Resizing", Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", memcached.Name, err)}) if err := r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } return ctrl.Result{}, err } // Now, that we update the size we want to requeue the reconciliation // so that we can ensure that we have the latest state of the resource before // update. Also, it will help ensure the desired state on the cluster return ctrl.Result{Requeue: true}, nil } // The following implementation will update the status meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionTrue, Reason: "Reconciling", Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", memcached.Name, desiredReplicas)}) if err := r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } return ctrl.Result{}, nil } // SetupWithManager sets up the controller with the Manager. func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&cachev1alpha1.Memcached{}). Owns(&appsv1.Deployment{}). Named("memcached"). Complete(r) } // deploymentForMemcached returns a Memcached Deployment object func (r *MemcachedReconciler) deploymentForMemcached( memcached *cachev1alpha1.Memcached) (*appsv1.Deployment, error) { image := "memcached:1.6.26-alpine3.19" dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: memcached.Name, Namespace: memcached.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: memcached.Spec.Size, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app.kubernetes.io/name": "project"}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app.kubernetes.io/name": "project"}, }, Spec: corev1.PodSpec{ SecurityContext: &corev1.PodSecurityContext{ RunAsNonRoot: ptr.To(true), SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, Containers: []corev1.Container{{ Image: image, Name: "memcached", ImagePullPolicy: corev1.PullIfNotPresent, // Ensure restrictive context for the container // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), RunAsUser: ptr.To(int64(1001)), AllowPrivilegeEscalation: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", }, }, }, Ports: []corev1.ContainerPort{{ ContainerPort: 11211, Name: "memcached", }}, Command: []string{"memcached", "--memory-limit=64", "-o", "modern", "-v"}, }}, }, }, }, } // Set the ownerRef for the Deployment // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil { return nil, err } return dep, nil } ================================================ FILE: docs/book/src/getting-started/testdata/project/internal/controller/memcached_controller_test.go ================================================ /* Copyright 2026 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 controller import ( "context" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/reconcile" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" cachev1alpha1 "example.com/memcached/api/v1alpha1" ) var _ = Describe("Memcached Controller", func() { Context("When reconciling a resource", func() { const resourceName = "test-resource" ctx := context.Background() typeNamespacedName := types.NamespacedName{ Name: resourceName, Namespace: "default", // TODO(user):Modify as needed } memcached := &cachev1alpha1.Memcached{} BeforeEach(func() { By("creating the custom resource for the Kind Memcached") err := k8sClient.Get(ctx, typeNamespacedName, memcached) if err != nil && errors.IsNotFound(err) { resource := &cachev1alpha1.Memcached{ ObjectMeta: metav1.ObjectMeta{ Name: resourceName, Namespace: "default", }, Spec: cachev1alpha1.MemcachedSpec{ Size: ptr.To(int32(1)), }, } Expect(k8sClient.Create(ctx, resource)).To(Succeed()) } }) AfterEach(func() { // TODO(user): Cleanup logic after each test, like removing the resource instance. resource := &cachev1alpha1.Memcached{} err := k8sClient.Get(ctx, typeNamespacedName, resource) Expect(err).NotTo(HaveOccurred()) By("Cleanup the specific resource instance Memcached") Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) }) It("should successfully reconcile the resource", func() { By("Reconciling the created resource") controllerReconciler := &MemcachedReconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), } _, err := controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) By("Checking if Deployment was successfully created in the reconciliation") Eventually(func(g Gomega) { found := &appsv1.Deployment{} g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed()) }).Should(Succeed()) By("Reconciling the custom resource again") _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) By("Checking the latest Status Condition added to the Memcached instance") Expect(k8sClient.Get(ctx, typeNamespacedName, memcached)).To(Succeed()) var conditions []metav1.Condition Expect(memcached.Status.Conditions).To(ContainElement( HaveField("Type", Equal(typeAvailableMemcached)), &conditions)) Expect(conditions).To(HaveLen(1), "Multiple conditions of type %s", typeAvailableMemcached) Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "condition %s", typeAvailableMemcached) Expect(conditions[0].Reason).To(Equal("Reconciling"), "condition %s", typeAvailableMemcached) }) }) }) ================================================ FILE: docs/book/src/getting-started/testdata/project/internal/controller/suite_test.go ================================================ /* Copyright 2026 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 controller import ( "context" "os" "path/filepath" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" cachev1alpha1 "example.com/memcached/api/v1alpha1" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var ( ctx context.Context cancel context.CancelFunc testEnv *envtest.Environment cfg *rest.Config k8sClient client.Client ) func TestControllers(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Controller Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) var err error err = cachev1alpha1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } // Retrieve the first found binary directory to allow running tests from IDEs if getFirstFoundEnvTestBinaryDir() != "" { testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() } // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) }) var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. // ENVTEST-based tests depend on specific binaries, usually located in paths set by // controller-runtime. When running tests directly (e.g., via an IDE) without using // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. // // This function streamlines the process by finding the required binaries, similar to // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are // properly set up, run 'make setup-envtest' beforehand. func getFirstFoundEnvTestBinaryDir() string { basePath := filepath.Join("..", "..", "bin", "k8s") entries, err := os.ReadDir(basePath) if err != nil { logf.Log.Error(err, "Failed to read directory", "path", basePath) return "" } for _, entry := range entries { if entry.IsDir() { return filepath.Join(basePath, entry.Name()) } } return "" } ================================================ FILE: docs/book/src/getting-started/testdata/project/test/e2e/e2e_suite_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2026 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 e2e import ( "fmt" "os" "os/exec" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "example.com/memcached/test/utils" ) var ( // managerImage is the manager image to be built and loaded for testing. managerImage = "example.com/project:v0.0.1" // shouldCleanupCertManager tracks whether CertManager was installed by this suite. shouldCleanupCertManager = false ) // TestE2E runs the e2e test suite to validate the solution in an isolated environment. // The default setup requires Kind and CertManager. // // To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true func TestE2E(t *testing.T) { RegisterFailHandler(Fail) _, _ = fmt.Fprintf(GinkgoWriter, "Starting project e2e test suite\n") RunSpecs(t, "e2e suite") } var _ = BeforeSuite(func() { By("building the manager image") cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage)) _, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image") // TODO(user): If you want to change the e2e test vendor from Kind, // ensure the image is built and available, then remove the following block. By("loading the manager image on Kind") err = utils.LoadImageToKindClusterWithName(managerImage) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind") setupCertManager() }) var _ = AfterSuite(func() { teardownCertManager() }) // setupCertManager installs CertManager if needed for webhook tests. // Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present. func setupCertManager() { if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" { _, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n") return } By("checking if CertManager is already installed") if utils.IsCertManagerCRDsInstalled() { _, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n") return } // Mark for cleanup before installation to handle interruptions and partial installs. shouldCleanupCertManager = true By("installing CertManager") Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") } // teardownCertManager uninstalls CertManager if it was installed by setupCertManager. // This ensures we only remove what we installed. func teardownCertManager() { if !shouldCleanupCertManager { _, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n") return } By("uninstalling CertManager") utils.UninstallCertManager() } ================================================ FILE: docs/book/src/getting-started/testdata/project/test/e2e/e2e_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2026 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 e2e import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "example.com/memcached/test/utils" ) // namespace where the project is deployed in const namespace = "project-system" // serviceAccountName created for the project const serviceAccountName = "project-controller-manager" // metricsServiceName is the name of the metrics service of the project const metricsServiceName = "project-controller-manager-metrics-service" // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data const metricsRoleBindingName = "project-metrics-binding" var _ = Describe("Manager", Ordered, func() { var controllerPodName string // Before running the tests, set up the environment by creating the namespace, // enforce the restricted security policy to the namespace, installing CRDs, // and deploying the controller. BeforeAll(func() { By("creating manager namespace") cmd := exec.Command("kubectl", "create", "ns", namespace) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") By("labeling the namespace to enforce the restricted security policy") cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, "pod-security.kubernetes.io/enforce=restricted") _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") By("installing CRDs") cmd = exec.Command("make", "install") _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") By("deploying the controller-manager") cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") }) // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, // and deleting the namespace. AfterAll(func() { By("cleaning up the curl pod for metrics") cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) _, _ = utils.Run(cmd) By("undeploying the controller-manager") cmd = exec.Command("make", "undeploy") _, _ = utils.Run(cmd) By("uninstalling CRDs") cmd = exec.Command("make", "uninstall") _, _ = utils.Run(cmd) By("removing manager namespace") cmd = exec.Command("kubectl", "delete", "ns", namespace) _, _ = utils.Run(cmd) }) // After each test, check for failures and collect logs, events, // and pod descriptions for debugging. AfterEach(func() { specReport := CurrentSpecReport() if specReport.Failed() { By("Fetching controller manager pod logs") cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) controllerLogs, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) } By("Fetching Kubernetes events") cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") eventsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err) } By("Fetching curl-metrics logs") cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) metricsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err) } By("Fetching controller manager pod description") cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) podDescription, err := utils.Run(cmd) if err == nil { fmt.Println("Pod description:\n", podDescription) } else { fmt.Println("Failed to describe controller pod") } } }) SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) Context("Manager", func() { It("should run successfully", func() { By("validating that the controller-manager pod is running as expected") verifyControllerUp := func(g Gomega) { // Get the name of the controller-manager pod cmd := exec.Command("kubectl", "get", "pods", "-l", "control-plane=controller-manager", "-o", "go-template={{ range .items }}"+ "{{ if not .metadata.deletionTimestamp }}"+ "{{ .metadata.name }}"+ "{{ \"\\n\" }}{{ end }}{{ end }}", "-n", namespace, ) podOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") podNames := utils.GetNonEmptyLines(podOutput) g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") controllerPodName = podNames[0] g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) // Validate the pod's status cmd = exec.Command("kubectl", "get", "pods", controllerPodName, "-o", "jsonpath={.status.phase}", "-n", namespace, ) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") } Eventually(verifyControllerUp).Should(Succeed()) }) It("should ensure the metrics endpoint is serving metrics", func() { By("creating a ClusterRoleBinding for the service account to allow access to metrics") cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, "--clusterrole=project-metrics-reader", fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), ) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") By("validating that the metrics service is available") cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") By("getting the service account token") token, err := serviceAccountToken() Expect(err).NotTo(HaveOccurred()) Expect(token).NotTo(BeEmpty()) By("ensuring the controller pod is ready") verifyControllerPodReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace, "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("True"), "Controller pod not ready") } Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying that the controller manager is serving the metrics server") verifyMetricsServerStarted := func(g Gomega) { cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(ContainSubstring("Serving metrics server"), "Metrics server not yet started") } Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed()) // +kubebuilder:scaffold:e2e-metrics-webhooks-readiness By("creating the curl-metrics pod to access the metrics endpoint") cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", "--namespace", namespace, "--image=curlimages/curl:latest", "--overrides", fmt.Sprintf(`{ "spec": { "containers": [{ "name": "curl", "image": "curlimages/curl:latest", "command": ["/bin/sh", "-c"], "args": [ "for i in $(seq 1 30); do curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics && exit 0 || sleep 2; done; exit 1" ], "securityContext": { "readOnlyRootFilesystem": true, "allowPrivilegeEscalation": false, "capabilities": { "drop": ["ALL"] }, "runAsNonRoot": true, "runAsUser": 1000, "seccompProfile": { "type": "RuntimeDefault" } } }], "serviceAccountName": "%s" } }`, token, metricsServiceName, namespace, serviceAccountName)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") By("waiting for the curl-metrics pod to complete.") verifyCurlUp := func(g Gomega) { cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", "-o", "jsonpath={.status.phase}", "-n", namespace) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") } Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) By("getting the metrics by checking curl-metrics logs") verifyMetricsAvailable := func(g Gomega) { metricsOutput, err := getMetricsOutput() g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") g.Expect(metricsOutput).NotTo(BeEmpty()) g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) } Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed()) }) // +kubebuilder:scaffold:e2e-webhooks-checks // TODO: Customize the e2e test suite with scenarios specific to your project. // Consider applying sample/CR(s) and check their status and/or verifying // the reconciliation by using the metrics, i.e.: // metricsOutput, err := getMetricsOutput() // Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") // Expect(metricsOutput).To(ContainSubstring( // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, // strings.ToLower(), // )) }) }) // serviceAccountToken returns a token for the specified service account in the given namespace. // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request // and parsing the resulting token from the API response. func serviceAccountToken() (string, error) { const tokenRequestRawString = `{ "apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest" }` // Temporary file to store the token request secretName := fmt.Sprintf("%s-token-request", serviceAccountName) tokenRequestFile := filepath.Join("/tmp", secretName) err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) if err != nil { return "", err } var out string verifyTokenCreation := func(g Gomega) { // Execute kubectl command to create the token cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( "/api/v1/namespaces/%s/serviceaccounts/%s/token", namespace, serviceAccountName, ), "-f", tokenRequestFile) output, err := cmd.CombinedOutput() g.Expect(err).NotTo(HaveOccurred()) // Parse the JSON output to extract the token var token tokenRequest err = json.Unmarshal(output, &token) g.Expect(err).NotTo(HaveOccurred()) out = token.Status.Token } Eventually(verifyTokenCreation).Should(Succeed()) return out, err } // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. func getMetricsOutput() (string, error) { By("getting the curl-metrics logs") cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) return utils.Run(cmd) } // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, // containing only the token field that we need to extract. type tokenRequest struct { Status struct { Token string `json:"token"` } `json:"status"` } ================================================ FILE: docs/book/src/getting-started/testdata/project/test/utils/utils.go ================================================ /* Copyright 2026 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 utils import ( "bufio" "bytes" "fmt" "os" "os/exec" "strings" . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck ) const ( certmanagerVersion = "v1.20.0" certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" defaultKindBinary = "kind" defaultKindCluster = "kind" ) func warnError(err error) { _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) } // Run executes the provided command within this context func Run(cmd *exec.Cmd) (string, error) { dir, _ := GetProjectDir() cmd.Dir = dir if err := os.Chdir(cmd.Dir); err != nil { _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err) } cmd.Env = append(os.Environ(), "GO111MODULE=on") command := strings.Join(cmd.Args, " ") _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command) output, err := cmd.CombinedOutput() if err != nil { return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err) } return string(output), nil } // UninstallCertManager uninstalls the cert manager func UninstallCertManager() { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } // Delete leftover leases in kube-system (not cleaned by default) kubeSystemLeases := []string{ "cert-manager-cainjector-leader-election", "cert-manager-controller", } for _, lease := range kubeSystemLeases { cmd = exec.Command("kubectl", "delete", "lease", lease, "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0") if _, err := Run(cmd); err != nil { warnError(err) } } } // InstallCertManager installs the cert manager bundle. func InstallCertManager() error { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "apply", "-f", url) if _, err := Run(cmd); err != nil { return err } // Wait for cert-manager-webhook to be ready, which can take time if cert-manager // was re-installed after uninstalling on a cluster. cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", "--for", "condition=Available", "--namespace", "cert-manager", "--timeout", "5m", ) _, err := Run(cmd) return err } // IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed // by verifying the existence of key CRDs related to Cert Manager. func IsCertManagerCRDsInstalled() bool { // List of common Cert Manager CRDs certManagerCRDs := []string{ "certificates.cert-manager.io", "issuers.cert-manager.io", "clusterissuers.cert-manager.io", "certificaterequests.cert-manager.io", "orders.acme.cert-manager.io", "challenges.acme.cert-manager.io", } // Execute the kubectl command to get all CRDs cmd := exec.Command("kubectl", "get", "crds") output, err := Run(cmd) if err != nil { return false } // Check if any of the Cert Manager CRDs are present crdList := GetNonEmptyLines(output) for _, crd := range certManagerCRDs { for _, line := range crdList { if strings.Contains(line, crd) { return true } } } return false } // LoadImageToKindClusterWithName loads a local docker image to the kind cluster func LoadImageToKindClusterWithName(name string) error { cluster := defaultKindCluster if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { cluster = v } kindOptions := []string{"load", "docker-image", name, "--name", cluster} kindBinary := defaultKindBinary if v, ok := os.LookupEnv("KIND"); ok { kindBinary = v } cmd := exec.Command(kindBinary, kindOptions...) _, err := Run(cmd) return err } // GetNonEmptyLines converts given command output string into individual objects // according to line breakers, and ignores the empty elements in it. func GetNonEmptyLines(output string) []string { var res []string elements := strings.SplitSeq(output, "\n") for element := range elements { if element != "" { res = append(res, element) } } return res } // GetProjectDir will return the directory where the project is func GetProjectDir() (string, error) { wd, err := os.Getwd() if err != nil { return wd, fmt.Errorf("failed to get current working directory: %w", err) } wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } // UncommentCode searches for target in the file and remove the comment prefix // of the target content. The target content may span multiple lines. func UncommentCode(filename, target, prefix string) error { // false positive // nolint:gosec content, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } strContent := string(content) idx := strings.Index(strContent, target) if idx < 0 { return fmt.Errorf("unable to find the code %q to be uncommented", target) } out := new(bytes.Buffer) _, err = out.Write(content[:idx]) if err != nil { return fmt.Errorf("failed to write to output: %w", err) } scanner := bufio.NewScanner(bytes.NewBufferString(target)) if !scanner.Scan() { return nil } for { if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil { return fmt.Errorf("failed to write to output: %w", err) } // Avoid writing a newline in case the previous line was the last in target. if !scanner.Scan() { break } if _, err = out.WriteString("\n"); err != nil { return fmt.Errorf("failed to write to output: %w", err) } } if _, err = out.Write(content[idx+len(target):]); err != nil { return fmt.Errorf("failed to write to output: %w", err) } // false positive // nolint:gosec if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil { return fmt.Errorf("failed to write file %q: %w", filename, err) } return nil } ================================================ FILE: docs/book/src/getting-started.md ================================================ # Getting Started We will create a sample project to let you know how it works. This sample will: - Reconcile a Memcached CR - which represents an instance of a Memcached deployed/managed on cluster - Create a Deployment with the Memcached image - Not allow more instances than the size defined in the CR which will be applied - Update the Memcached CR status ## Create a project First, create and navigate into a directory for your project. Then, initialize it using `kubebuilder`: ```shell mkdir $GOPATH/memcached-operator cd $GOPATH/memcached-operator kubebuilder init --domain=example.com ``` ## Create the Memcached API (CRD): Next, we'll create the API which will be responsible for deploying and managing Memcached(s) instances on the cluster. ```shell kubebuilder create api --group cache --version v1alpha1 --kind Memcached ``` ### Understanding APIs This command's primary aim is to produce the Custom Resource (CR) and Custom Resource Definition (CRD) for the Memcached Kind. It creates the API with the group `cache.example.com` and version `v1alpha1`, uniquely identifying the new CRD of the Memcached Kind. By leveraging the Kubebuilder tool, we can define our APIs and objects representing our solutions for these platforms. While we've added only one Kind of resource in this example, we can have as many `Groups` and `Kinds` as necessary. To make it easier to understand, think of CRDs as the definition of our custom Objects, while CRs are instances of them. ### Defining our API #### Defining the Specs Now, we will define the values that each instance of your Memcached resource on the cluster can assume. In this example, we will allow configuring the number of instances with the following: ```go type MemcachedSpec struct { ... // +kubebuilder:validation:Minimum=0 // +required Size *int32 `json:"size,omitempty"` } ``` #### Creating Status definitions We also want to track the status of our Operations which will be done to manage the Memcached CR(s). This allows us to verify the Custom Resource's description of our own API and determine if everything occurred successfully or if any errors were encountered, similar to how we do with any resource from the Kubernetes API. ```go // MemcachedStatus defines the observed state of Memcached type MemcachedStatus struct { // +listType=map // +listMapKey=type // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` } ``` #### Markers and validations Furthermore, we want to validate the values added in our CustomResource to ensure that those are valid. To achieve this, we will use [markers][markers], such as `+kubebuilder:validation:Minimum=1`. Now, see our example fully completed. {{#literatego ./getting-started/testdata/project/api/v1alpha1/memcached_types.go}} #### Generating manifests with the specs and validations To generate all required files: 1. Run `make generate` to create the DeepCopy implementations in `api/v1alpha1/zz_generated.deepcopy.go`. 2. Then, run `make manifests` to generate the CRD manifests under `config/crd/bases` and a sample for it under `config/samples`. Both commands use [controller-gen][controller-gen] with different flags for code and manifest generation, respectively.
config/crd/bases/cache.example.com_memcacheds.yaml: Our Memcached CRD ```yaml {{#include ./getting-started/testdata/project/config/crd/bases/cache.example.com_memcacheds.yaml}} ```
#### Sample of Custom Resources The manifests located under the `config/samples` directory serve as examples of Custom Resources that can be applied to the cluster. In this particular example, by applying the given resource to the cluster, we would generate a Deployment with a single instance size (see `size: 1`). ```yaml {{#include ./getting-started/testdata/project/config/samples/cache_v1alpha1_memcached.yaml}} ``` ### Reconciliation Process In a simplified way, Kubernetes works by allowing us to declare the desired state of our system, and then its controllers continuously observe the cluster and take actions to ensure that the actual state matches the desired state. For our custom APIs and controllers, the process is similar. Remember, we are extending Kubernetes' behaviors and its APIs to fit our specific needs. In our controller, we will implement a reconciliation process. Essentially, the reconciliation process functions as a loop, continuously checking conditions and performing necessary actions until the desired state is achieved. This process will keep running until all conditions in the system align with the desired state defined in our implementation. Here's a pseudo-code example to illustrate this: ```go reconcile App { // Check if a Deployment for the app exists, if not, create one // If there's an error, then restart from the beginning of the reconcile if err != nil { return reconcile.Result{}, err } // Check if a Service for the app exists, if not, create one // If there's an error, then restart from the beginning of the reconcile if err != nil { return reconcile.Result{}, err } // Look for Database CR/CRD // Check the Database Deployment's replicas size // If deployment.replicas size doesn't match cr.size, then update it // Then, restart from the beginning of the reconcile. For example, by returning `reconcile.Result{Requeue: true}, nil`. if err != nil { return reconcile.Result{Requeue: true}, nil } ... // If at the end of the loop: // Everything was executed successfully, and the reconcile can stop return reconcile.Result{}, nil } ``` #### In the context of our example When our sample Custom Resource (CR) is applied to the cluster (i.e. `kubectl apply -f config/sample/cache_v1alpha1_memcached.yaml`), we want to ensure that a Deployment is created for our Memcached image and that it matches the number of replicas defined in the CR. To achieve this, we need to first implement an operation that checks whether the Deployment for our Memcached instance already exists on the cluster. If it does not, the controller will create the Deployment accordingly. Therefore, our reconciliation process must include an operation to ensure that this desired state is consistently maintained. This operation would involve: ```go // Check if the deployment already exists, if not create a new one found := &appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found) if err != nil && apierrors.IsNotFound(err) { // Define a new deployment dep := r.deploymentForMemcached() // Create the Deployment on the cluster if err = r.Create(ctx, dep); err != nil { log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) return ctrl.Result{}, err } ... } ``` Next, note that the `deploymentForMemcached()` function will need to define and return the Deployment that should be created on the cluster. This function should construct the Deployment object with the necessary specifications, as demonstrated in the following example: ```go dep := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: &replicas, Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{{ Image: "memcached:1.6.26-alpine3.19", Name: "memcached", ImagePullPolicy: corev1.PullIfNotPresent, Ports: []corev1.ContainerPort{{ ContainerPort: 11211, Name: "memcached", }}, Command: []string{"memcached", "--memory-limit=64", "-o", "modern", "-v"}, }}, }, }, }, } ``` Additionally, we need to implement a mechanism to verify that the number of Memcached replicas on the cluster matches the desired count specified in the Custom Resource (CR). If there is a discrepancy, the reconciliation must update the cluster to ensure consistency. This means that whenever a CR of the Memcached Kind is created or updated on the cluster, the controller will continuously reconcile the state until the actual number of replicas matches the desired count. The following example illustrates this process: ```go ... size := memcached.Spec.Size if *found.Spec.Replicas != size { found.Spec.Replicas = &size if err = r.Update(ctx, found); err != nil { log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) return ctrl.Result{}, err } ... ``` Now, you can review the complete controller responsible for managing Custom Resources of the Memcached Kind. This controller ensures that the desired state is maintained in the cluster, making sure that our Memcached instance continues running with the number of replicas specified by the users.
internal/controller/memcached_controller.go: Our Controller Implementation ```go {{#include ./getting-started/testdata/project/internal/controller/memcached_controller.go}} ```
### Diving Into the Controller Implementation #### Setting Manager to Watching Resources The whole idea is to be Watching the resources that matter for the controller. When a resource that the controller is interested in changes, the Watch triggers the controller's reconciliation loop, ensuring that the actual state of the resource matches the desired state as defined in the controller's logic. Notice how we configured the Manager to monitor events such as the creation, update, or deletion of a Custom Resource (CR) of the Memcached kind, as well as any changes to the Deployment that the controller manages and owns: ```go // SetupWithManager sets up the controller with the Manager. // The Deployment is also watched to ensure its // desired state in the cluster. func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). // Watch the Memcached Custom Resource and trigger reconciliation whenever it //is created, updated, or deleted For(&cachev1alpha1.Memcached{}). // Watch the Deployment managed by the Memcached controller. If any changes occur to the Deployment // owned and managed by this controller, it will trigger reconciliation, ensuring that the cluster // state aligns with the desired state. Owns(&appsv1.Deployment{}). Complete(r) } ``` #### But, How Does the Manager Know Which Resources Are Owned by It? We do not want our Controller to watch any Deployment on the cluster and trigger our reconciliation loop. Instead, we only want to trigger reconciliation when the specific Deployment running our Memcached instance is changed. For example, if someone accidentally deletes our Deployment or changes the number of replicas, we want to trigger the reconciliation to ensure that it returns to the desired state. The Manager knows which Deployment to observe because we set the `ownerRef` (Owner Reference): ```go if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil { return nil, err } ``` ### Granting Permissions It's important to ensure that the Controller has the necessary permissions(i.e. to create, get, update, and list) the resources it manages. The [RBAC permissions][k8s-rbac] are now configured via [RBAC markers][rbac-markers], which are used to generate and update the manifest files present in `config/rbac/`. These markers can be found (and should be defined) on the `Reconcile()` method of each controller, see how it is implemented in our example: ```go // +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch ``` After making changes to the controller, run the make manifests command. This will prompt [controller-gen][controller-gen] to refresh the files located under `config/rbac`.
config/rbac/role.yaml: Our RBAC Role generated ```yaml {{#include ./getting-started/testdata/project/config/rbac/role.yaml}} ```
### Manager (main.go) The [Manager][manager] in the `cmd/main.go` file is responsible for managing the controllers in your application.
cmd/main.go: Our main.go ```go {{#include ./getting-started/testdata/project/cmd/main.go}} ```
### Use Kubebuilder plugins to scaffold additional options Now that you have a better understanding of how to create your own API and controller, let’s scaffold in this project the plugin [`autoupdate.kubebuilder.io/v1-alpha`][autoupdate-plugin] so that your project can be kept up to date with the latest Kubebuilder releases scaffolding changes and consequently adopt improvements from the ecosystem. ```shell kubebuilder edit --plugins="autoupdate/v1-alpha" ``` Inspect the file `.github/workflows/auto-update.yml` to see how it works. ### Checking the Project running in the cluster At this point you can check the steps to validate the project on the cluster by looking the steps defined in the Quick Start, see: [Run It On the Cluster](./quick-start#run-it-on-the-cluster) ## Next Steps - To delve deeper into developing your solution, consider going through the [CronJob Tutorial][cronjob-tutorial] - For insights on optimizing your approach, refer to the [Best Practices][best-practices] documentation. [k8s-operator-pattern]: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [group-kind-oh-my]: ./cronjob-tutorial/gvks.md [controller-gen]: ./reference/controller-gen.md [markers]: ./reference/markers.md [rbac-markers]: ./reference/markers/rbac.md [k8s-rbac]: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ [manager]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/manager [options-manager]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/manager#Options [quick-start]: ./quick-start.md [best-practices]: ./reference/good-practices.md [cronjob-tutorial]: https://book.kubebuilder.io/cronjob-tutorial/cronjob-tutorial.html [deploy-image]: ./plugins/available/deploy-image-plugin-v1-alpha.md [GOPATH-golang-docs]: https://golang.org/doc/code.html#GOPATH [go-modules-blogpost]: https://blog.golang.org/using-go-modules [autoupdate-plugin]: ./plugins/available/autoupdate-v1-alpha.md ================================================ FILE: docs/book/src/introduction.md ================================================ >[!Tip] >Impatient readers may head straight to [Quick Start](quick-start.md). >[!Important] >Using previous version of Kubebuilder? Check the legacy documentation for [v1](https://book-v1.book.kubebuilder.io), [v2](https://book-v2.book.kubebuilder.io) or [v3](https://book-v3.book.kubebuilder.io). ## Who is this for #### Users of Kubernetes Users of Kubernetes will develop a deeper understanding of Kubernetes through learning the fundamental concepts behind how APIs are designed and implemented. This book will teach readers how to develop their own Kubernetes APIs and the principles from which the core Kubernetes APIs are designed. Including: - The structure of Kubernetes APIs and Resources - API versioning semantics - Self-healing - Garbage Collection and Finalizers - Declarative vs Imperative APIs - Level-Based vs Edge-Base APIs - Resources vs Subresources #### Kubernetes API extension developers API extension developers will learn the principles and concepts behind implementing canonical Kubernetes APIs, as well as simple tools and libraries for rapid execution. This book covers pitfalls and misconceptions that extension developers commonly encounter. Including: - How to batch multiple events into a single reconciliation call - How to configure periodic reconciliation - *Forthcoming* - When to use the lister cache vs live lookups - Garbage Collection vs Finalizers - How to use Declarative vs Webhook Validation - How to implement API versioning ## Why Kubernetes APIs Kubernetes APIs provide consistent and well defined endpoints for objects adhering to a consistent and rich structure. This approach has fostered a rich ecosystem of tools and libraries for working with Kubernetes APIs. Users work with the APIs through declaring objects as *yaml* or *json* config, and using common tooling to manage the objects. Building services as Kubernetes APIs provides many advantages to plain old REST, including: * Hosted API endpoints, storage, and validation. * Rich tooling and CLIs such as `kubectl` and `kustomize`. * Support for AuthN and granular AuthZ. * Support for API evolution through API versioning and conversion. * Facilitation of adaptive / self-healing APIs that continuously respond to changes in the system state without user intervention. * Kubernetes as a hosting environment Developers may build and publish their own Kubernetes APIs for installation into running Kubernetes clusters. ## Contribution If you like to contribute to either this book or the code, please be so kind to read our [Contribution](https://github.com/kubernetes-sigs/kubebuilder/blob/master/CONTRIBUTING.md) guidelines first. ## Resources * Repository: [sigs.k8s.io/kubebuilder](https://sigs.k8s.io/kubebuilder) * Slack channel: [#kubebuilder](http://slack.k8s.io/#kubebuilder) * Google Group: [kubebuilder@googlegroups.com](https://groups.google.com/forum/#!forum/kubebuilder) ================================================ FILE: docs/book/src/logos/README.md ================================================ # Kubebuilder Logos The official location for the logos is in a [public GCS bucket][kb-logos-gcs] (or if you like GCS XML listings, [here][kb-logos-gcs-direct]). These logos are copies used in the book, resized to their appropriate sizes. [kb-logos-gcs]: https://console.cloud.google.com/storage/browser/kubebuilder-logos [kb-logos-gcs-direct]: https://storage.googleapis.com/kubebuilder-logos ================================================ FILE: docs/book/src/migration/ai-helpers.md ================================================ # Using AI to Migrate Projects from Any Version to the Latest AI can assist manual migrations by reducing repetitive work and helping resolve breaking changes. It won't replace the [Manual Migration Process](./manual-process.md), but it can help reduce effort and accomplish the goal. ## Workflow and AI-Assisted Steps **Step 1: Reorganize to New Layout** (required only for legacy layouts) AI helps ensure the project is structured with the new layout (main.go under cmd/, controllers and webhooks inside internal/). Review and verify the reorganization, then run `make build` to ensure it still compiles. See [Step 1: Reorganize to New Layout](./reorganize-layout.md) **Step 2: Discovery CLI Commands to Re-scaffold** AI analyzes your project and generates all Kubebuilder CLI commands to fully re-scaffold with the latest release. Create a backup (`mkdir ../migration-backup && cp -r . ../migration-backup/`), then execute the generated commands to scaffold a fresh project. See [Step 2: Discovery CLI Commands](./discovery-commands.md) **Step 3: Port Custom Code** AI helps port your custom code from backup to the new scaffolded project. Review all changes carefully and ensure business logic is correctly transferred. See [Step 3: Port Custom Code](./port-code.md) **Step 4: Validate** Run `make generate && make manifests && make build`, then `make test` to verify all tests pass. Deploy to a test cluster and verify your solution still does the same thing. See the [Manual Migration Process](./manual-process.md) for complete details. ================================================ FILE: docs/book/src/migration/discovery-commands.md ================================================ # Step 2: Discovery CLI Commands Use AI to analyze your (now reorganized) Kubebuilder project and generate all CLI commands needed to recreate it with the latest version. ## Instructions to provide to your AI assistant Copy and paste these instructions to your AI assistant (Cursor, Claude, GitHub Copilot, etc.): ``` Analyze this Kubebuilder project and generate all CLI commands to recreate it. CONTEXT: Kubebuilder projects have these components: APIs (Custom Resources): - Location: api/ or apis/ directory - Recognition: Look for Go structs with marker: // +kubebuilder:object:root=true - Pattern: type struct with metav1.TypeMeta and metav1.ObjectMeta fields - Example: type Captain struct { metav1.TypeMeta; metav1.ObjectMeta; Spec CaptainSpec; Status CaptainStatus } Controllers: - Location: controllers/, internal/controller/, or pkg/controllers/ - Recognition: Look for Reconcile() function signature - Pattern: func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) - Struct embeds: client.Client - Has function: SetupWithManager(mgr ctrl.Manager) error Webhooks: - Location: api/v1/ or internal/webhook/v1/ - Recognition: Look for webhook method signatures - Defaulting pattern: func Default() or func Default(ctx context.Context, obj *) error - Validation pattern: func ValidateCreate() error or func ValidateCreate(ctx context.Context, obj *) (admission.Warnings, error) - Conversion pattern: func Hub() or func ConvertTo() or func ConvertFrom() CLI Command Formats: - kubebuilder init --domain --repo - kubebuilder edit --multigroup=true (if multi-group layout) - kubebuilder create api --group --version --kind --controller= --resource= * --controller=true: create controller * --resource=true: create API definition * --resource=false: controller only (for external types like Deployment, Pod) - kubebuilder create webhook --group --version --kind [flags] * --defaulting: sets default values * --programmatic-validation: validates create/update/delete * --conversion --spoke : for multi-version APIs (hub-spoke pattern) - Hub version: Usually oldest stable version (e.g., v1) - command runs on this version - Spoke versions: Newer versions that convert to/from hub (e.g., v2, v3) - specified with --spoke - Example: --group crew --version v1 --kind Captain --conversion --spoke v2 (v1 is hub, v2 is spoke) - External types (k8s.io/api/*): use --resource=false --controller=true Project structure patterns: - Single-group: api/v1/, api/v2/ (versions directly under api/) - Multi-group: api//v1/, api//v2/ (group subdirectories) - Multi-group detection: Check PROJECT file for "multigroup: true" OR check if api/ has group subdirectories Files to IGNORE: - zz_generated.*.go (auto-generated code) - groupversion_info.go (just group registration) - config/crd/bases/*.yaml (auto-generated from code) - config/rbac/*.yaml (auto-generated from markers) References: - Kubebuilder Book: https://book.kubebuilder.io - controller-runtime: https://github.com/kubernetes-sigs/controller-runtime - controller-tools: https://github.com/kubernetes-sigs/controller-tools ANALYZE PROJECT: 1. Extract module path from go.mod (line 1: "module ") 2. Extract domain from PROJECT file (domain: ) OR api/*/groupversion_info.go (// +groupName=.) 3. Detect multi-group: api/ has api//v1/ structure? (yes/no) 4. Scan api/ or apis/ directory - Find ALL your own APIs: - Find all *_types.go files OR types.go (exclude groupversion_info.go, zz_generated.deepcopy.go) - For each file, find: type struct with // +kubebuilder:object:root=true above it - Extract: Kind name, group (from groupversion_info.go +groupName comment), version (from directory) - Check controller: look for controllers/_controller.go OR internal/controller/_controller.go OR pkg/controllers/_controller.go - Check webhooks: look for api/v1/_webhook.go OR internal/webhook/v1/_webhook.go - If webhook file found, scan for methods: * "func (r *) Default()": has --defaulting * "func (r *) ValidateCreate()": has --programmatic-validation * "func (*) Hub()": this version is conversion hub * "func (r *) ConvertTo(": this version is a spoke 5. Scan internal/controller/, controllers/, or pkg/controllers/ - Find controllers for external types: - For each *_controller.go file, check imports NOT from your module - Look for: k8s.io/api/apps/v1, k8s.io/api/core/v1, github.com/cert-manager/cert-manager/pkg/apis/* - Extract type from: type Reconciler struct OR Reconcile signature - This is a controller-only resource (use --controller=true --resource=false) 6. Scan internal/webhook/ - Find webhooks for external types: - For each *_webhook.go file in internal/webhook/v1/ (or other versions) - Check if the Kind type is imported (not defined in your api/) - If imported from k8s.io/api/* or external package: external type webhook - Scan for Default() and ValidateCreate() methods to determine flags OUTPUT FORMAT (bash script): #!/bin/bash # Module: # Domain: # Multi-group: set -e kubebuilder init --domain --repo kubebuilder edit --multigroup=true # only if multi-group # External type controllers (--resource=false) kubebuilder create api --group cert-manager --version v1 --kind Certificate \ --controller=true --resource=false \ --external-api-path= --external-api-domain= --external-api-module= # Your own APIs (--resource=true) kubebuilder create api --group crew --version v1 --kind Captain --controller=true --resource=true kubebuilder create api --group crew --version v2 --kind FirstMate --controller=false --resource=true # Webhooks for your own APIs kubebuilder create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation kubebuilder create webhook --group crew --version v1 --kind FirstMate --conversion --spoke v2 # Webhooks for external/core types (no create api needed) kubebuilder create webhook --group apps --version v1 --kind Deployment --defaulting --programmatic-validation kubebuilder create webhook --group core --version v1 --kind Pod --defaulting make manifests && make generate && make build RULES: - Combine ALL webhook types in ONE command: --defaulting --programmatic-validation together - Conversion webhooks: use hub version and list ALL spokes: --conversion --spoke v2,v3 - List EVERY Kind found in source code, not just what's in PROJECT file - External type controllers: use --controller=true --resource=false - Webhooks for external/core types: just create webhook (no create api needed) - Order: external controllers first, then your APIs, then all webhooks ``` ## Understanding the Output The AI will analyze your project and output a bash script. The script will contain commands in this order: 1. `kubebuilder init` - Initialize the project 2. `kubebuilder edit --multigroup=true` - If multi-group detected 3. `kubebuilder create api` - For external type controllers (with `--resource=false`) 4. `kubebuilder create api` - For your own APIs (with `--resource=true`) 5. `kubebuilder create webhook` - For all webhooks 6. `make manifests && make generate && make build` - Verify ## Example Outputs Here are real examples of what the AI instructions generate: ### Example 1: Simple Multi-Group Project Analyzed: [kubernetes-sigs/scheduler-plugins](https://github.com/kubernetes-sigs/scheduler-plugins) ```bash #!/bin/bash # Module: sigs.k8s.io/scheduler-plugins # Domain: scheduling.x-k8s.io # Multi-group: YES set -e kubebuilder init --domain scheduling.x-k8s.io --repo sigs.k8s.io/scheduler-plugins kubebuilder edit --multigroup=true kubebuilder create api --group scheduling --version v1alpha1 --kind ElasticQuota --controller=true --resource=true kubebuilder create api --group scheduling --version v1alpha1 --kind PodGroup --controller=true --resource=true make manifests && make generate && make build ``` **Discovered:** 2 APIs, multi-group, no webhooks ### Example 2: Single-Group with Webhooks (go/v3 Migration) Analyzed: [project-v3](https://github.com/kubernetes-sigs/kubebuilder/tree/release-3.13/testdata/project-v3) ```bash #!/bin/bash # Module: sigs.k8s.io/kubebuilder/testdata/project-v3 # Domain: testproject.org # Multi-group: NO set -e kubebuilder init --domain testproject.org --repo sigs.k8s.io/kubebuilder/testdata/project-v3 kubebuilder create api --group crew --version v1 --kind Captain --controller=true --resource=true kubebuilder create api --group crew --version v1 --kind FirstMate --controller=true --resource=true kubebuilder create api --group crew --version v1 --kind Admiral --controller=true --resource=true kubebuilder create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation kubebuilder create webhook --group crew --version v1 --kind Admiral --defaulting make manifests && make generate && make build ``` **Discovered:** 3 APIs, single-group, webhooks with defaulting and validation ### Example 3: Complex Multi-Group with External Types Analyzed: testdata/project-v4-multigroup ```bash #!/bin/bash # Module: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup # Domain: testproject.org # Multi-group: YES set -e kubebuilder init --domain testproject.org --repo sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup kubebuilder edit --multigroup=true # External type controllers kubebuilder create api --group cert-manager --version v1 --kind Certificate \ --controller=true --resource=false \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=io \ --external-api-module=github.com/cert-manager/cert-manager@v1.19.2 kubebuilder create api --group apps --version v1 --kind Deployment --controller=true --resource=false # APIs - Group: crew kubebuilder create api --group crew --version v1 --kind Captain --controller=true --resource=true # APIs - Group: ship kubebuilder create api --group ship --version v1beta1 --kind Frigate --controller=true --resource=true kubebuilder create api --group ship --version v1 --kind Destroyer --controller=true --resource=true kubebuilder create api --group ship --version v2alpha1 --kind Cruiser --controller=true --resource=true # APIs - Group: sea-creatures kubebuilder create api --group sea-creatures --version v1beta1 --kind Kraken --controller=true --resource=true kubebuilder create api --group sea-creatures --version v1beta2 --kind Leviathan --controller=true --resource=true # APIs - Group: foo.policy kubebuilder create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true # APIs - Group: foo kubebuilder create api --group foo --version v1 --kind Bar --controller=true --resource=true # APIs - Group: fiz kubebuilder create api --group fiz --version v1 --kind Bar --controller=true --resource=true # APIs - Group: example.com kubebuilder create api --group example.com --version v1alpha1 --kind Memcached --controller=true --resource=true kubebuilder create api --group example.com --version v1alpha1 --kind Busybox --controller=true --resource=true kubebuilder create api --group example.com --version v1 --kind Wordpress --controller=true --resource=true kubebuilder create api --group example.com --version v2 --kind Wordpress --controller=false --resource=true # Webhooks for your APIs kubebuilder create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation kubebuilder create webhook --group ship --version v1 --kind Destroyer --defaulting kubebuilder create webhook --group ship --version v2alpha1 --kind Cruiser --programmatic-validation kubebuilder create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation kubebuilder create webhook --group example.com --version v1 --kind Wordpress --conversion --spoke v2 # Webhooks for external types kubebuilder create webhook --group cert-manager --version v1 --kind Issuer --defaulting kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation kubebuilder create webhook --group apps --version v1 --kind Deployment --defaulting --programmatic-validation make manifests && make generate && make build ``` **Discovered:** 12 APIs across 6 groups, conversion webhook, external controllers, external webhooks ## What to Do Next 1. Review the generated script carefully and ensure it matches your project structure. 2. Save it as `migration-commands.sh` and make it executable: `chmod +x migration-commands.sh` 3. Follow the [Manual Migration Process](./manual-process.md) to: - Backup your project in another location - Execute the commands of this script in the root of your project when it is empty - After you have the fully re-scaffolded project, you will need to add all your code back on top of it - Port your custom code ================================================ FILE: docs/book/src/migration/manual-process.md ================================================ # Manual Migration Process Please ensure you have followed the [installation guide][quick-start] to install the required components and have the desired version of the Kubebuilder CLI available in your `PATH`. This guide outlines the manual steps to migrate your existing Kubebuilder project to a newer version of the Kubebuilder framework. This process involves re-scaffolding your project and manually porting over your custom code and configurations. From Kubebuilder `v3.0.0` onwards, all inputs used by Kubebuilder are tracked in the [PROJECT][project-config] file. Ensure that you check this file in your current project to verify the recorded configuration and metadata. Review the [PROJECT file documentation][project-config] for a better understanding. Also, before starting, it is recommended to check [What's in a basic project?][basic-project-doc] to better understand the project layouts and structure. ## Phase 1: Reorganize to New Layout (Required only for Legacy Layouts) **Only needed if ANY of these are true:** - Controllers are NOT in `internal/controller/` - Webhooks are NOT in `internal/webhook/` - Main is NOT in `cmd/` **Skip this phase if** your project already uses `internal/controller/`, `internal/webhook/`, and `cmd/main.go`. ### 1.1 Create a reorganization branch ```bash git checkout -b reorganize ``` ### 1.2 Reorganize file locations Move files to new layout: ```bash # If you have controllers/ directory mkdir -p internal/controller mv controllers/* internal/controller/ rmdir controllers # OR if you have pkg/controllers/ directory mkdir -p internal/controller mv pkg/controllers/* internal/controller/ # If you have webhooks in api/v1/ or apis/v1/ mkdir -p internal/webhook/v1 mv api/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || mv apis/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || echo "No webhook files found to move (this is expected if your project has no webhooks)" # If main.go is in root mkdir -p cmd mv main.go cmd/ ``` ### 1.3 Update package declarations After moving files, update package declarations: **Controllers:** Change `package controllers` → `package controller` in all `*_controller.go` and `*_controller_test.go` files. **Webhooks:** Keep version as package name (e.g., `package v1` stays `package v1` in `internal/webhook/v1/`). ### 1.4 Update import paths Find and update all imports: ```bash grep -r "pkg/controllers\|/controllers\"" --include="*.go" ``` In each file found, update: - Imports: `/controllers` or `/pkg/controllers` → `/internal/controller` - References: `controllers.TypeName` → `controller.TypeName` ### 1.5 Update Dockerfile (if needed) If your Dockerfile has explicit COPY statements for moved paths, update them to reflect the new structure, or simplify to `COPY . .` and use `.dockerignore` to exclude unnecessary files. ### 1.6 Verify and commit Build and test the reorganized project: ```bash make generate manifests make build && make test ``` If successful, commit the layout changes. Your project now uses the new layout. Proceed to Phase 2. ## Phase 2: Migrate to Latest Version ### Step 1: Prepare Your Current Project ### 1.1 Create a migration branch Create a branch from your current codebase: ```bash git checkout -b migration ``` ### 1.2 Create a backup ```bash mkdir ../migration-backup cp -r . ../migration-backup/ ``` ### 1.3 Clean your project directory Remove all files except `.git`: ```bash find . -not -path './.git*' -not -name '.' -not -name '..' -delete ``` ## Step 2: Initialize the New Project **About the PROJECT file:** From v3.0.0+, the `PROJECT` file tracks all scaffolding metadata. If you have one and used CLI for all resources, try `kubebuilder alpha generate` first. Otherwise, follow the manual steps below to identify and re-scaffold all resources. ### 2.1 Identify your module and domain Identify the information you'll need for initialization from your backup. **Module path** - Check your backup's `go.mod` file: ```bash cat ../migration-backup/go.mod ``` Look for the module line: ```go module tutorial.kubebuilder.io/migration-project ``` **Domain** - Check your backup's `PROJECT` file: ```bash cat ../migration-backup/PROJECT ``` Look for the domain line: ```yaml domain: tutorial.kubebuilder.io ``` If you don't have a `PROJECT` file (versions < `v3.0.0`), check your CRD files under `config/crd/bases/` or examine the API group names. The domain is the part after the group name in your API groups. ### 2.2 Initialize the Go module Initialize a new Go module using the same module path from your original project: ```bash go mod init tutorial.kubebuilder.io/migration-project ``` Replace `tutorial.kubebuilder.io/migration-project` with your actual module path. ### 2.3 Initialize Kubebuilder project Initialize the project with Kubebuilder: ```bash kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/migration-project ``` Replace with your actual domain and repository (module path). ### 2.4 Enable multi-group support (if needed) Multi-group projects organize APIs into different groups, with each group in its own directory. This is useful when you have APIs for different purposes or domains. **Check if your project uses multi-group layout** by examining your backup's directory structure: - **Single-group layout:** All APIs in one group - `api/v1/cronjob_types.go` - `api/v1/job_types.go` - `api/v2/cronjob_types.go` - **Multi-group layout:** APIs organized by group - `api/batch/v1/cronjob_types.go` - `api/crew/v1/captain_types.go` - `api/sea/v1/ship_types.go` You can also check your backup's `PROJECT` file for `multigroup: true`. **If your project uses multi-group layout**, enable it before creating APIs: ```bash kubebuilder edit --multigroup=true ``` When following this guide, you'll get the new layout automatically since you're creating a fresh project with the latest version and porting your code into it. ## Step 3: Re-scaffold APIs and Controllers For each API resource in your original project, re-scaffold them in the new project. ### 3.1 Identify all your APIs Review your backup project (`../migration-backup/`) to identify all APIs. **It's recommended to check the backup directory regardless of whether you have a `PROJECT` file**, as not all resources may have been created using the CLI. **Check the directory structure** in your backup to ensure you don't miss any manually created resources: - Look in the `api/` directory (or `apis/` for projects generated with older Kubebuilder versions) for `*_types.go` files: - Single-group: `api/v1/cronjob_types.go` - extract: version `v1`, kind `CronJob`, group from imports - Multi-group: `api/batch/v1/cronjob_types.go` - extract: group `batch`, version `v1`, kind `CronJob` - Check for controllers in these locations: - **Current:** `internal/controller/cronjob_controller.go` or `internal/controller//cronjob_controller.go` - **Legacy:** `controllers/cronjob_controller.go` or `pkg/controllers/cronjob_controller.go` **If you used the CLI to create all APIs from Kubebuilder `v3.0.0+` you should have them in the `PROJECT` file** under the `resources` section, such as: ```yaml resources: - api: crdVersion: v1 namespaced: true controller: true group: batch kind: CronJob version: v1 ``` ### 3.2 Create each API and Controller For each API identified in step 3.1, re-scaffold it: ```bash kubebuilder create api --group batch --version v1 --kind CronJob ``` When prompted: - Answer **yes** to "Create Resource [y/n]" to generate the API types - Answer **yes** to "Create Controller [y/n]" if your original project has a controller for this API **After creating each API**, update the generated manifests and code: ```bash make manifests # Generate CRD, RBAC, and other config files make generate # Generate code (e.g., DeepCopy methods) ``` Then verify everything compiles: ```bash make build ``` These steps ensure the newly scaffolded API is properly integrated. See the [Quick Start][quick-start] guide for a detailed walkthrough of the API creation workflow. Repeat this process for **ALL** APIs in your project. After creating all resources, regenerate manifests: ```bash make manifests make generate ``` ### 3.3 Re-scaffold webhooks (if applicable) If your original project has webhooks, you need to re-scaffold them. **Identify webhooks in your backup project:** 1. **From directory structure**, look for webhook files: - Legacy location (v3 and earlier): `api/v1/_webhook.go` or `api///_webhook.go` - Current location (single-group): `internal/webhook//_webhook.go` - Current location (multi-group): `internal/webhook///_webhook.go` 2. **From `PROJECT` file** (if available), check each resource's webhooks section: ```yaml resources: - api: ... webhooks: defaulting: true validation: true webhookVersion: v1 ``` **Re-scaffold webhooks:** For each resource with webhooks, run: ```bash kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation ``` **Webhook options:** - `--defaulting` - creates a defaulting webhook (sets default values) - `--programmatic-validation` - creates a validation webhook (validates create/update/delete operations) - `--conversion` - creates a conversion webhook (for multi-version APIs, see next section) ### 3.4 Re-scaffold conversion webhooks (if applicable) If your project has multi-version APIs with conversion webhooks, you need to set up the hub-spoke conversion pattern. **Setting up conversion webhooks:** Create the conversion webhook for the **hub** version, with spoke versions specified using the `--spoke` flag. **Note:** In the examples below, we use `v1` as the hub for illustration. Choose the version in your project that should be the central conversion point—typically your most feature-complete and stable storage version, not necessarily the oldest or newest. ```bash kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2 ``` This command: - Creates conversion webhook for `v1` as the **hub** version - Configures `v2` as a **spoke** that converts to/from the hub `v1` - Generates `*_conversion.go` files with conversion method stubs **For multiple spokes**, specify them as a comma-separated list: ```bash kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2,v1alpha1 ``` This sets up `v1` as the **hub** with both `v2` and `v1alpha1` as **spokes**. **What you need to implement:** The command generates method stubs that you'll fill in during Step 4: - **Hub version**: Implement `Hub()` method (usually just a marker) - **Spoke versions**: Implement `ConvertTo(hub)` and `ConvertFrom(hub)` methods with your conversion logic See the [Multi-Version Tutorial][multiversion-tutorial] for comprehensive guidance on implementing the conversion logic. After scaffolding all webhooks, verify everything compiles: ```bash make manifests && make build ``` ## Step 4: Port Your Custom Code Manually port your custom business logic and configurations from the backup to the new project. ### 4.1 Port API definitions Compare and merge your custom API fields and markers from your backup project. **Files to compare:** - **Single-group:** `api/v1/_types.go` - **Multi-group:** `api///_types.go` **What to port:** 1. **Custom fields** in Spec and Status structs 2. **Validation markers** - e.g., `+kubebuilder:validation:Minimum=0`, `+kubebuilder:validation:Pattern=...` 3. **CRD generation markers** - e.g., `+kubebuilder:printcolumn`, `+kubebuilder:resource:scope=Cluster` 4. **SubResources** - e.g., `+kubebuilder:subresource:status`, `+kubebuilder:subresource:scale` 5. **Documentation comments** - Used for CRD descriptions See [CRD Generation][crd-generation], [CRD Validation][crd-validation], and [Markers][markers] for all available markers. **If your APIs reference a parent package** (e.g., `scheduling.GroupName`), port it: ```bash mkdir -p api// cp ../migration-backup/apis//groupversion_info.go api// ``` After porting API definitions, regenerate and verify: ```bash make manifests # Generate CRD manifests from your types make generate # Generate DeepCopy methods ``` This ensures your API types and CRD manifests are properly generated before moving forward. ### 4.2 Port controller logic **Files to compare:** - **Current single-group:** `internal/controller/_controller.go` - **Current multi-group:** `internal/controller//_controller.go` **What to port:** 1. **Reconcile function implementation** - Your core business logic 2. **Helper functions** - Any additional functions in the controller file 3. **RBAC markers** - `+kubebuilder:rbac:groups=...,resources=...,verbs=...` 4. **Additional watches** - Custom watch configurations in `SetupWithManager` 5. **Imports** - Any additional packages your controller needs 6. **Struct fields** - Custom fields added to the Reconciler struct See [RBAC Markers][rbac-markers] for details on permission markers. After porting controller logic, regenerate manifests and verify compilation: ```bash make generate make manifests make build ``` ### 4.3 Port webhook implementations Webhooks have changed location between Kubebuilder versions. Be aware of the path differences: **Legacy webhook location** (Kubebuilder v3 and earlier): - `api/v1/_webhook.go` - `api///_webhook.go` **Current webhook location:** - Single-group: `internal/webhook//_webhook.go` - Multi-group: `internal/webhook///_webhook.go` **What to port:** 1. **Defaulting webhook** - `Default()` method implementation 2. **Validation webhook** - `ValidateCreate()`, `ValidateUpdate()`, `ValidateDelete()` methods 3. **Conversion webhook** - `ConvertTo()` and `ConvertFrom()` methods (for multi-version APIs) 4. **Helper functions** - Any validation or defaulting helper functions 5. **Webhook markers** - Usually auto-generated, but verify they match your needs See [Webhook Overview][webhook-overview], [Admission Webhook][admission-webhook], and the [Multi-Version Tutorial][multiversion-tutorial] for details. **For conversion webhooks:** If you have conversion webhooks, ensure you used the `create webhook --conversion --spoke ` command in Step 3.4. This sets up the hub-spoke infrastructure automatically. You only need to fill in the conversion logic in the `ConvertTo()` and `ConvertFrom()` methods in your spoke versions, and the `Hub()` method in your hub version. The command creates all the necessary boilerplate - you just implement the business logic for converting fields between versions. After porting webhooks, regenerate and verify: ```bash make generate make manifests make build ``` ### 4.4 Port main.go customizations (if any) **File:** `cmd/main.go` Most projects don't need to customize `main.go` as Kubebuilder handles all the standard setup automatically (registering APIs, setting up controllers and webhooks, manager initialization, metrics, etc.). Only port customizations that are not part of the standard scaffold. Compare your backup `main.go` with the new scaffolded one to identify any custom logic you added. ### 4.5 Configure Kustomize manifests The `config/` directory contains Kustomize manifests for deploying your operator. Compare with your backup to ensure all configurations are properly set up. **Review and update these directories:** 1. **`config/default/kustomization.yaml`** - Main kustomization file - Ensure webhook configurations are enabled if you have webhooks (uncomment webhook-related patches) - Ensure cert-manager is enabled if using webhooks (uncomment certmanager resources) - Enable or disable metrics endpoint based on your original configuration - Review namespace and name prefix settings 2. **`config/manager/`** - Controller manager deployment - Usually no changes are needed unless you have customizations. In that case, compare resource limits and requests with your backup and check environment variables 3. **`config/rbac/`** - RBAC configurations - Usually auto-generated from markers - no manual changes needed - Only check if you have custom role bindings or service account configurations not covered by markers 4. **`config/webhook/`** - Webhook configurations (if applicable) - Usually auto-generated - no manual changes needed - Only check if you have custom webhook service or certificate configurations 5. **`config/samples/`** - Sample CR manifests - Copy your sample resources from the backup After configuring Kustomize, verify the manifests build correctly: ```bash make all make build-installer ``` ### 4.6 Port additional customizations Port any additional packages, dependencies, and customizations from your backup: **Additional packages** (e.g., `pkg/util`): ```bash cp -r ../migration-backup/pkg/ pkg/ # Update import paths (works on both macOS and Linux) find pkg/ -name "*.go" -exec sed -i.bak 's|/apis/|/api/|g' {} \; find pkg/ -name "*.go.bak" -delete ``` For dependencies, run `go mod tidy` or copy `go.mod`/`go.sum` from backup for complex projects. Check for additional customizations (Makefile, Dockerfile, test files). Use diff tools to compare with backup and identify missed files. After porting all customizations, verify everything builds: ```bash make all ``` ## Step 5: Test and Verify Compare against the backup to ensure all customizations were correctly ported, such as: ```bash diff -r --brief ../migration-backup/ . | grep "Only in ../migration-backup" ``` Run tests and verify functionality: ```bash make test && make lint-fix ``` Deploy to a test cluster (e.g. [kind][kind-doc]) and verify the changes (i.e. validate expected behavior, run regression checks, confirm the full CI pipeline still passes, and execute the e2e tests). ## Additional Resources - [Migration Overview](../migrations.md) - Overview of all migration options - [PROJECT File Reference][project-config] - Understanding the PROJECT file - [What's in a basic project?][basic-project-doc] - Understanding project structure - [Alpha Generate Command](../reference/commands/alpha_generate.md) - Automated re-scaffolding - [Alpha Update Command](../reference/commands/alpha_update.md) - Automated migration - [Using External Types][external-types] - Controllers for types not defined in your project - [CRD Generation][crd-generation] - Generating CRDs from Go types - [CRD Validation][crd-validation] - Adding validation to your APIs - [Markers][markers] - All available markers for code generation - [RBAC Markers][rbac-markers] - Generating RBAC manifests - [Webhook Overview][webhook-overview] - Understanding webhooks - [Admission Webhook][admission-webhook] - Implementing admission webhooks - [Multi-Version Tutorial][multiversion-tutorial] - Handling multiple API versions - [Deploying cert-manager][cert-manager] - Required for webhooks - [Configuring EnvTest][envtest] - Testing with EnvTest [quick-start]: ../quick-start.md [project-config]: ../reference/project-config.md [basic-project-doc]: ../cronjob-tutorial/basic-project.md [external-types]: ../reference/using_an_external_resource.md [external-types-webhooks]: ../reference/using_an_external_resource.md#creating-a-webhook-to-manage-an-external-type [crd-generation]: ../reference/generating-crd.md [crd-validation]: ../reference/markers/crd-validation.md [markers]: ../reference/markers.md [rbac-markers]: ../reference/markers/rbac.md [webhook-overview]: ../reference/webhook-overview.md [admission-webhook]: ../reference/admission-webhook.md [multiversion-tutorial]: ../multiversion-tutorial/tutorial.md [cert-manager]: ../cronjob-tutorial/cert-manager.md [envtest]: ../reference/envtest.md [standard-go-project]: https://github.com/golang-standards/project-layout [kind-doc]: ../reference/kind.md [autoupdate-plugin]: ../plugins/available/autoupdate-v1-alpha.md [alpha-update]: ../reference/commands/alpha_update.md ================================================ FILE: docs/book/src/migration/multi-group.md ================================================ # Single Group to Multi-Group Kubebuilder scaffolds single-group projects by default to keep things simple, as most projects don't require multiple API groups. However, you can convert an existing single-group project to use multi-group layout when needed. This reorganizes your APIs and controllers into group-specific directories. See the [design doc][multigroup-design] for the rationale behind this design decision. ## Understanding the Layouts Here's what changes when you go from single-group to multi-group: **Single-group layout (default):** ``` api//*_types.go All your CRD schemas in one place internal/controller/* All your controllers together internal/webhook//* Webhooks organized by version (if you have any) ``` **Multi-group layout:** ``` api///*_types.go CRD schemas organized by group internal/controller//* Controllers organized by group internal/webhook///* Webhooks organized by group and version (if you have any) ``` You can tell which layout you're using by checking your `PROJECT` file for `multigroup: true`. ## Migration Steps The following steps migrate the [CronJob example][cronjob-tutorial] from single-group to multi-group layout. ### Step 1: Enable multi-group mode First, tell Kubebuilder you want to use multi-group layout: ```bash kubebuilder edit --multigroup=true ``` This command updates your `PROJECT` file by adding `multigroup: true`. After this change: - **New APIs** you create will automatically use the multi-group structure (`api///`) - **Existing APIs** remain in their current location and must be migrated manually (steps 3-9 below) ### Step 2: Identify your group name Check `api/v1/groupversion_info.go` to find your group name: ```go // +groupName=batch.tutorial.kubebuilder.io package v1 ``` The group name is the first part before the dot (`batch` in this example). ### Step 3: Move your APIs Create a directory for your group and move your version directories: ```bash mkdir -p api/batch mv api/v1 api/batch/ ``` If you have multiple versions (like `v1`, `v2`, etc.), move them all: ```bash mv api/v2 api/batch/ ``` ### Step 4: Move your controllers Create a group directory and move all controller files: ```bash mkdir -p internal/controller/batch mv internal/controller/*.go internal/controller/batch/ ``` This will move all your controller files, including `suite_test.go`, into the group directory. Each group needs its own test suite. ### Step 5: Move your webhooks (if you have any) If your project has webhooks (check for an `internal/webhook/` directory), add the group directory: ```bash mkdir -p internal/webhook/batch mv internal/webhook/v1 internal/webhook/batch/ mv internal/webhook/v2 internal/webhook/batch/ # if v2 exists ``` If you don't have webhooks, skip this step. ### Step 6: Update import paths Update all import statements to point to the new locations. **What used to look like this:** ```go import ( batchv1 "tutorial.kubebuilder.io/project/api/v1" "tutorial.kubebuilder.io/project/internal/controller" ) ``` **Should now look like this:** ```go import ( batchv1 "tutorial.kubebuilder.io/project/api/batch/v1" batchcontroller "tutorial.kubebuilder.io/project/internal/controller/batch" ) ``` **If you have webhooks, you'll also need to update those imports:** ```go // Before webhookv1 "tutorial.kubebuilder.io/project/internal/webhook/v1" // After webhookbatchv1 "tutorial.kubebuilder.io/project/internal/webhook/batch/v1" ``` Files to check and update: - `cmd/main.go` - `internal/controller/batch/*.go` - `internal/webhook/batch/v1/*.go` (if you have webhooks) - `api/batch/v1/*_test.go` Tip: Use your IDE's "Find and Replace" feature across the project. ### Step 7: Update the PROJECT file The `kubebuilder edit --multigroup=true` command sets `multigroup: true` in your PROJECT file but doesn't update paths for existing APIs. You need to manually update the `path` field for each resource. **Verify your PROJECT file has these changes:** 1. **Check that `multigroup: true` is set** (at the top level): ```yaml layout: - go.kubebuilder.io/v4 multigroup: true # Must be true projectName: project ``` 2. **Update the `path` field for each resource**: **Before:** ```yaml resources: - api: crdVersion: v1 namespaced: true controller: true group: batch kind: CronJob path: tutorial.kubebuilder.io/project/api/v1 # Old path version: v1 ``` **After:** ```yaml resources: - api: crdVersion: v1 namespaced: true controller: true group: batch kind: CronJob path: tutorial.kubebuilder.io/project/api/batch/v1 # New path with group version: v1 ``` Repeat this for **all resources** in your PROJECT file. ### Step 8: Update test suite CRD paths Update the CRD directory path in test suites. Since files moved one level deeper, add one more `".."` to the path. **In `internal/controller/batch/suite_test.go`:** **Before (was at `internal/controller/suite_test.go`):** ```go testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, } ``` **After (now at `internal/controller/batch/suite_test.go`):** ```go testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, } ``` **If you have webhooks, update `internal/webhook/batch/v1/webhook_suite_test.go`:** **Before (was at `internal/webhook/v1/webhook_suite_test.go`):** ```go testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, } ``` **After (now at `internal/webhook/batch/v1/webhook_suite_test.go`):** ```go testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")}, } ``` ### Step 9: Verify the migration Run the following commands to verify everything works: ```bash make manifests # Regenerate CRDs and RBAC make generate # Regenerate code make test # Run tests make build # Build the project ``` ## AI-Assisted Migration If you're using an AI coding assistant (Cursor, GitHub Copilot, etc.), you can automate most of the migration steps. [gvks]: /cronjob-tutorial/gvks.md "Groups and Versions and Kinds, oh my!" [cronjob-tutorial]: /cronjob-tutorial/cronjob-tutorial.md "Tutorial: Building CronJob" [multigroup-design]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/simplified-scaffolding.md ================================================ FILE: docs/book/src/migration/namespace-scoped.md ================================================ # Migrating to Namespace-Scoped Manager This guide covers converting **existing cluster-scoped projects** to namespace-scoped deployment. By default, Kubebuilder scaffolds cluster-scoped managers that watch and manage resources across all namespaces. This guide shows how to convert an existing cluster-scoped project to namespace-scoped deployment, limiting the manager to watch only specific namespace(s). ## When to Use Namespace-Scoped **Use namespace-scoped when:** - Building tenant-specific managers in multi-tenant clusters - Security policies require least-privilege (no cluster-wide permissions) - Need multiple manager instances in different namespaces - Managing only namespace-scoped resources (Deployments, Services, ConfigMaps, etc.) **Use cluster-scoped (default) when:** - Managing cluster-scoped resources (Nodes, ClusterRoles, Namespaces, etc.) - Single manager instance managing resources across all namespaces ## Migration Steps **Quick Summary:** 1. Run `kubebuilder edit --namespaced --force` - scaffolds Role/RoleBinding and updates manager.yaml 2. Update cmd/main.go to configure namespace-scoped cache 3. Add `namespace=` parameter to RBAC markers in existing controller files 4. Run `make manifests` - regenerate RBAC from updated markers 5. Verify and deploy ## Detailed Steps: ### 1. Enable namespace-scoped mode ```bash kubebuilder edit --namespaced --force ``` This command automatically: - Sets `namespaced: true` in your PROJECT file - Scaffolds `config/rbac/role.yaml` with `kind: Role` (namespace-scoped) - Scaffolds `config/rbac/role_binding.yaml` with `kind: RoleBinding` - Regenerates `config/manager/manager.yaml` with WATCH_NAMESPACE environment variable - Regenerates admin/editor/viewer roles with `kind: Role` (namespace-scoped) for all existing APIs **Note:** The `--force` flag regenerates config/manager/manager.yaml. Without `--force`, you must manually add WATCH_NAMESPACE (see below). ### 2. Update cmd/main.go (Required Manual Step) The edit command cannot update cmd/main.go automatically. You must manually add namespace-scoped configuration. **a. Add import:** ```go import ( // ... existing imports ... "sigs.k8s.io/controller-runtime/pkg/cache" ) ``` **b. Add helper functions (after `init()` and before `main()`):** ```go // getWatchNamespace returns the namespace(s) the manager should watch for changes. // It reads the value from the WATCH_NAMESPACE environment variable. func getWatchNamespace() (string, error) { watchNamespaceEnvVar := "WATCH_NAMESPACE" ns, found := os.LookupEnv(watchNamespaceEnvVar) if !found { return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar) } return ns, nil } // setupCacheNamespaces configures the cache to watch specific namespace(s). func setupCacheNamespaces(namespaces string) cache.Options { defaultNamespaces := make(map[string]cache.Config) for _, ns := range strings.Split(namespaces, ",") { defaultNamespaces[strings.TrimSpace(ns)] = cache.Config{} } return cache.Options{ DefaultNamespaces: defaultNamespaces, } } ``` **c. In `main()` function, before `ctrl.NewManager()`, add:** ```go // Get the namespace(s) for namespace-scoped mode from WATCH_NAMESPACE environment variable. watchNamespace, err := getWatchNamespace() if err != nil { setupLog.Error(err, "Unable to get WATCH_NAMESPACE") os.Exit(1) } ``` **d. Update manager creation to use namespace-scoped cache:** ```go mgrOptions := ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "your-leader-election-id", // ... other existing options ... } // Configure cache to watch namespace(s) specified in WATCH_NAMESPACE mgrOptions.Cache = setupCacheNamespaces(watchNamespace) setupLog.Info("Watching namespace(s)", "namespaces", watchNamespace) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions) if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) } ``` ### 3. Update RBAC markers in existing controllers For each **existing controller file**, add the `namespace=` parameter to RBAC markers. **Find controller files:** - Look for files containing `func (r *SomeReconciler) Reconcile(` - Common locations: `internal/controller/*_controller.go` In `internal/controller/cronjob_controller.go`: **Before (cluster-scoped):** ```go // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ``` **After (namespace-scoped):** ```go // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,namespace=-system,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,namespace=-system,resources=cronjobs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,namespace=-system,resources=cronjobs/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ``` Replace `project-system` with your namespace (found in `config/default/kustomization.yaml` under the `namespace:` field). ### 4. Regenerate RBAC manifests After updating RBAC markers in Step 3, regenerate the RBAC manifests: ```bash make manifests # Regenerate RBAC from updated controller markers ``` Verify the generated files show `kind: Role` instead of `kind: ClusterRole`: **config/rbac/role.yaml:** ```yaml kind: Role metadata: name: manager-role # Note: namespace is added by kustomize during build, not in source ``` **config/rbac/*_editor_role.yaml, *_viewer_role.yaml, *_admin_role.yaml:** ```yaml kind: Role metadata: name: cronjob-editor-role # Note: namespace is added by kustomize during build, not in source ``` ### 5. Verify and deploy Run tests to verify everything works: ```bash make generate # Regenerate code make test # Run tests ``` Deploy and verify: ```bash make deploy IMG= # Verify RBAC is namespace-scoped (not cluster-scoped) kubectl get role,rolebinding -n # Test: Create a resource in the manager's namespace - should be reconciled kubectl apply -f config/samples/ -n # Test: Create a resource in a different namespace - should NOT be reconciled kubectl apply -f config/samples/ -n other-namespace ``` ## AI-Assisted Migration If you're using an AI coding assistant (Cursor, GitHub Copilot, etc.), you can automate the manual migration steps. ## Multi-Namespace Support The `WATCH_NAMESPACE` environment variable supports comma-separated values to watch multiple specific namespaces: ```yaml env: - name: WATCH_NAMESPACE value: "namespace-1,namespace-2,namespace-3" ``` Note: You'll need to create Role/RoleBinding in each namespace for proper RBAC. ## Reverting to Cluster-Scoped To revert back to cluster-scoped: ```bash kubebuilder edit --namespaced=false --force ``` This command automatically: - Sets `namespaced: false` in your PROJECT file - Scaffolds `config/rbac/role.yaml` with `kind: ClusterRole` - Scaffolds `config/rbac/role_binding.yaml` with `kind: ClusterRoleBinding` - With `--force`: Regenerates `config/manager/manager.yaml` without WATCH_NAMESPACE env var **Manual steps required:** 1. Remove `namespace=` parameter from RBAC markers in all controller files 2. Run `make manifests` to regenerate cluster-scoped RBAC 3. Remove namespace-scoped code from `cmd/main.go`: - Remove `getWatchNamespace()` function - Remove `setupCacheNamespaces()` function - Remove namespace retrieval and cache configuration - Remove added imports (`fmt`, `strings`, `cache`) if not used elsewhere 4. If you didn't use `--force`, manually remove `WATCH_NAMESPACE` from `config/manager/manager.yaml` ## Important Notes - **Only controllers need RBAC updates**: Only update `+kubebuilder:rbac` markers in controller files (files with `Reconcile` function). Webhook files do NOT use RBAC markers - webhooks use certificate-based authentication with the API server. - **RBAC markers control scope**: The `namespace=` parameter in controller RBAC markers determines whether controller-gen generates `Role` (namespace-scoped) or `ClusterRole` (cluster-scoped). Without the `namespace=` parameter, controller-gen always generates `ClusterRole`. - **Controller-gen regenerates role.yaml**: After running `make manifests`, controller-gen will regenerate `config/rbac/role.yaml` based on your controller RBAC markers. The initial `Role` scaffold from `kubebuilder edit --namespaced=true` serves as a template, but controller-gen manages the actual content. - **Namespace parameter format**: Use `namespace=` in controller RBAC markers, typically `namespace=-system` to match your deployment namespace. - **Metrics auth role stays cluster-scoped**: The `metrics-auth-role` uses cluster-scoped APIs (TokenReview, SubjectAccessReview) and correctly remains a ClusterRole without namespace parameter. - **Webhooks require manual configuration**: Currently, controller-gen does not support `namespaceSelector` or `objectSelector` markers for webhooks. See the webhook section above for details. ## See Also - [Manager Scope](../reference/manager-scope.md) - Detailed explanation of manager scope concepts - [Project Config](../reference/project-config.md) - PROJECT file configuration reference ================================================ FILE: docs/book/src/migration/port-code.md ================================================ # Step 3: Port Custom Code After reorganizing your project (Step 1) and executing scaffolding commands from discovery (Step 2), use AI to port your custom code to the new project. ## Instructions to provide to your AI assistant Copy and paste these instructions to your AI assistant: ``` Port custom code from Kubebuilder project backup to new scaffolded project. CONTEXT: What is scaffold vs custom: - Scaffold: Auto-generated boilerplate by Kubebuilder (has "// TODO(user):" comments) - Custom: Your business logic that replaces TODOs Backup location: ../migration-backup/ (your old project with custom code) New project: . (newly scaffolded project with TODOs to replace) How to recognize each file type (by content, not just name): API files (typically *_types.go): - Have marker: // +kubebuilder:object:root=true - Have structs: type struct with metav1.TypeMeta, metav1.ObjectMeta - Have: Spec struct (desired state) - Have: Status struct (observed state) - Markers like: // +kubebuilder:validation:... Controller files (typically *_controller.go): - Have struct: type Reconciler struct { client.Client; Scheme *runtime.Scheme } - Have function: func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) - Have function: func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error - May have: // +kubebuilder:rbac markers before Reconcile Webhook files (typically *_webhook.go): - OLD pattern: func (r *) Default(), func (r *) ValidateCreate() error - NEW pattern: type CustomDefaulter struct, func (d *CustomDefaulter) Default(ctx context.Context, obj *) error - Conversion: func (*) Hub(), func (r *) ConvertTo(...), func (r *) ConvertFrom(...) Main file: - Has: func main() - Has: ctrl.NewManager(...) - Registers controllers and webhooks File paths after Step 1: - APIs in: api/v1/ or api//v1/ - Controllers in: internal/controller/ or internal/controller// - Webhooks in: internal/webhook/v1/ or internal/webhook//v1/ - Main: cmd/main.go Files to NEVER edit (auto-generated): - config/crd/bases/*.yaml (generated from make manifests) - config/rbac/role.yaml (generated from make manifests) - config/webhook/manifests.yaml (generated from make manifests) - **/zz_generated.*.go (generated from make generate) - PROJECT file (managed by CLI) Critical markers to NEVER remove: - // +kubebuilder:scaffold:* (Kubebuilder injects code here) Make command sequence: - After editing APIs or markers: make generate && make manifests - After editing Go code: make build - After all changes: make lint-fix && make generate && make manifests && make all && make test Common markers in API files: - // +kubebuilder:validation:Required - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:validation:Pattern="^[a-z]+$" - // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=... RBAC markers in controller files: - // +kubebuilder:rbac:groups=,resources=,verbs=get;list;watch;create;update;patch;delete - // +kubebuilder:rbac:groups=,resources=/status,verbs=get;update;patch - // +kubebuilder:rbac:groups=,resources=/finalizers,verbs=update References: - Kubebuilder Book: https://book.kubebuilder.io - Markers Reference: https://book.kubebuilder.io/reference/markers.html - controller-runtime: https://github.com/kubernetes-sigs/controller-runtime - controller-tools: https://github.com/kubernetes-sigs/controller-tools PORT CUSTOM CODE (in this order): 1. Port go.mod dependencies FIRST: Compare ../migration-backup/go.mod with current go.mod a. For packages in backup but NOT in new (exclude k8s.io/*, sigs.k8s.io/controller-*): - Run: go get @ b. For packages in BOTH with different versions: - Keep the HIGHER (newer) version - If backup has newer version: go get @ - If new scaffold has newer version: keep it (don't downgrade) - NOTE: Old projects can have newer versions than scaffold After ALL: run go mod tidy 2. Port API type definitions: For each *_types.go in backup to new (paths match after Step 1): Backup: ../migration-backup/api/v1/_types.go New: api/v1/_types.go Port: - Custom fields in Spec and Status structs - ALL +kubebuilder markers (validation, printcolumn, resource, etc.) - Documentation comments - Custom types (enums, type aliases) - REMOVE "// TODO(user):" comments when adding fields NEVER remove: // +kubebuilder:scaffold:* or // +kubebuilder:object:root=true After each: go mod tidy && make generate && make manifests 3. Port controller implementations: For each controller (paths match after Step 1): Backup: ../migration-backup/internal/controller/_controller.go New: internal/controller/_controller.go Port in order: a. Additional imports (ADD to existing) b. Custom constants, variables, types, interfaces (before Reconciler struct) c. Custom fields in Reconciler struct d. ALL +kubebuilder:rbac markers (place before Reconcile) e. Reconcile() body (REMOVE "// TODO(user):" and paste custom logic) f. ALL helper functions (closures and standalone) g. SetupWithManager customizations (if any beyond default .For().Named().Complete()) After each: go mod tidy && make generate && make manifests && make build 4. Port webhooks: CRITICAL: Code pattern depends on controller-runtime version! Webhooks (paths match after Step 1): Backup: ../migration-backup/internal/webhook/v1/_webhook.go New: internal/webhook/v1/_webhook.go Detect pattern by reading backup file: - Has "func (r *) Default() {": OLD pattern (needs adaptation) - Has "func (d *CustomDefaulter) Default(ctx": NEW pattern (direct copy) IF OLD pattern - ADAPT: - Default(): Extract logic, paste after type assertion, change 'r.' to '.', add return nil, REMOVE TODO - Validate*(): Extract logic, paste after assertion, change 'r.' to '.', change return types, REMOVE TODO - Conversion: Copy Hub/ConvertTo/ConvertFrom directly (no change needed) IF NEW pattern - DIRECT COPY: - Copy CustomDefaulter/CustomValidator structs and all methods - Copy helper functions and imports After each: go mod tidy && make manifests && make build 5. Port main.go customizations: Backup: ../migration-backup/cmd/main.go New: cmd/main.go Compare and port ONLY custom additions: - Custom manager options - Custom command-line flags - Custom initialization before mgr.Start() - Additional scheme registrations DO NOT port standard scaffold (controller/webhook setup, manager config) After: make build 6. Port config settings (ADAPT, don't copy): a. config/default/kustomization.yaml - Compare and adapt: - Uncomment webhook/certmanager if you have webhooks - Update namespace/namePrefix if custom - Match metrics configuration - Add custom patches/resources DO NOT copy entire file b. Other config/*/kustomization.yaml - Check for custom patches, adapt if needed c. Custom config dirs - Copy any additional dirs: config/dev/, config/prod/, etc. After: make build-installer 7. Port config samples and customizations: - Sample CRs: Copy ../migration-backup/config/samples/*.yaml to config/samples/ - Makefile: Copy custom targets from backup (preserve scaffolded targets) - Dockerfile: Apply custom build steps from backup 8. Port ALL tests: - Controller tests: Copy *_controller_test.go from backup - Webhook tests: Copy *_webhook_test.go (adapt if pattern changed) - E2E tests: Copy test/e2e/* if exist - Integration tests: Copy test/integration/* if exist 9. Port additional files: - README: Port custom sections (don't replace entire file) - Additional dirs: Copy docs/, scripts/, examples/, charts/, testdata/ if exist - Root files: Copy .env, VERSION, CHANGELOG.md, CONTRIBUTING.md if exist - .github workflows: Copy custom workflows DO NOT port: dist/, bin/, vendor/ 10. Verify nothing missed: - Run: diff -r --brief ../migration-backup/ . | grep "Only in ../migration-backup" - Port any custom files found (ignore: .git/, bin/, vendor/, dist/, zz_generated.*, go.sum, auto-gen configs) - Verify key files have custom code (APIs, controllers, webhooks) 11. Final verification: - Run: go mod tidy - Run: make lint-fix - Run: make generate - Run: make manifests - Run: make build - Run: make build-installer - Run: make test Success: no errors, tests pass, functionally identical to backup IMPORTANT REMINDERS: - NEVER edit auto-generated files (already listed in CONTEXT above) - NEVER remove // +kubebuilder:scaffold:* comments - REMOVE "// TODO(user):" when replacing with custom code - ADAPT config YAML files, don't copy entire files - Port EVERYTHING except: .git/, bin/, vendor/, dist/, zz_generated.*, go.sum - Follow make command sequence from CONTEXT above ``` ## What AI Will Do The AI will: 1. **Detect layouts** - Compare old and new project structures 2. **Port API definitions** - Custom fields, markers, documentation 3. **Port controller logic** - Imports, types, Reconcile(), helpers, RBAC, SetupWithManager 4. **Adapt webhooks** - Handle pattern changes if needed, port all logic and helpers 5. **Port main.go** - Only custom initialization, flags, and manager options 6. **Port configs** - kustomization.yaml, samples, Makefile, Dockerfile 7. **Port dependencies** - Add packages to go.mod, run go mod tidy 8. **Port tests** - Controller tests, webhook tests, e2e tests, integration tests 9. **Port additional files** - README, docs/, scripts/, .github/, any custom directories 10. **Verify completely** - Run lint-fix, generate, manifests, build, test ## After AI Completes **Critical: Review carefully!** ## Example: What Gets Ported ### API Custom Fields **From backup** (`api/v1/captain_types.go`): ```go type CaptainSpec struct { // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=100 Replicas int32 `json:"replicas"` // +kubebuilder:validation:Pattern=`^[a-z]+$` Name string `json:"name"` } ``` **To new project** (TODO removed, custom fields added): ```go type CaptainSpec struct { // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=100 Replicas int32 `json:"replicas"` // +kubebuilder:validation:Pattern=`^[a-z]+$` Name string `json:"name"` } ``` ### Controller Reconcile Logic **From backup** (Reconcile function body): ```go func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) // Your custom reconciliation logic here var captain crewv1.Captain if err := r.Get(ctx, req.NamespacedName, &captain); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Custom business logic... return ctrl.Result{}, nil } ``` **To new project** (TODO removed, custom logic added): ```go func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := log.FromContext(ctx) // Custom reconciliation logic from backup var captain crewv1.Captain if err := r.Get(ctx, req.NamespacedName, &captain); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Custom business logic... return ctrl.Result{}, nil } ``` ### Webhook Adaptation (v3 to v4) **From go/v3 backup**: ```go func (r *Captain) Default() { if r.Spec.Replicas == 0 { r.Spec.Replicas = 1 } } ``` **To go/v4 new project**: ```go func (d *CaptainCustomDefaulter) Default(ctx context.Context, obj *crewv1.Captain) error { // Ported logic adapted (obj is type-safe, no assertion needed): if obj.Spec.Replicas == 0 { obj.Spec.Replicas = 1 } return nil } ``` ## Next Steps After AI ports your code: 1. Check if nothing is missed, broken or wrongly ported 2. Deploy to test cluster - Verify behavior ================================================ FILE: docs/book/src/migration/reorganize-layout.md ================================================ # Step 1: Reorganize to New Layout (Required only for Legacy Layouts) **If your project was built with Kubebuilder prior to v3.0.0**, you will probably need this step. Reorganize files to match the new directory layout. **Check if you need this step (if ANY are true, you need this):** - Controllers are NOT in `internal/controller/` - Webhooks are NOT in `internal/webhook/` - Main is NOT in `cmd/` **If ALL are already in the new layout**, skip to [Step 2](./discovery-commands.md) ## Instructions to provide to your AI assistant Copy and paste these instructions to your AI assistant: ``` Reorganize Kubebuilder project files to match new directory layout. CONTEXT: - Project location: . (current directory - your existing project) - Goal: Move files to new layout WITHOUT changing code or versions - Keep project functional after reorganization STEP 1 - Check which files need to move: - Controllers in controllers/ or pkg/controllers/: needs move - Controllers in internal/controller/ or internal/controller// (multi-group): already correct - Webhooks in api/v1/ or apis//v1/: needs move - Webhooks in internal/webhook/v1/ or internal/webhook//v1/ (multi-group): already correct - Main in root (main.go): needs move - Main in cmd/ (cmd/main.go or cmd/*/main.go): already correct STEP 2 - Reorganize file locations: a. Move controllers if needed: - If controllers/ directory exists: mkdir -p internal/controller mv controllers/* internal/controller/ rmdir controllers - If pkg/controllers/ directory exists: mkdir -p internal/controller mv pkg/controllers/* internal/controller/ b. Move webhooks if needed: - If api/v1/ or apis/v1/ contains *_webhook.go files: mkdir -p internal/webhook/v1 mv api/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || mv apis/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || true - If api//v1/ or apis//v1/ contains webhooks (multi-group): mkdir -p internal/webhook//v1 mv api//v1/*_webhook* internal/webhook//v1/ 2>/dev/null || mv apis//v1/*_webhook* internal/webhook//v1/ 2>/dev/null || true c. Move main.go if needed: - If main.go exists in root: mkdir -p cmd mv main.go cmd/ STEP 3 - Update import paths in ALL files: After moving files, imports will break. Fix them systematically: a. In cmd/main.go (or cmd/*/main.go, cmd/*/*.go): - Find: import "your-module/controllers" - Replace with: import "your-module/internal/controller" - Find: import "your-module/pkg/controllers" - Replace with: import "your-module/internal/controller" - Find: &controllers.SomeReconciler or controllers.NewController - Replace with: &controller.SomeReconciler or controller.NewController - API imports (api/v1, apis/v1alpha1) - NO CHANGE needed b. In internal/controller/*.go files: - Check package declaration is still: package controller (not controllers) - API imports stay same - NO CHANGE needed - If you had controller-to-controller imports, update paths c. In internal/webhook/v1/*.go files: - Check package declaration: should be package v1 - API imports stay same - NO CHANGE needed - Webhook imports in main.go may need updating STEP 4 - Update Dockerfile (if using explicit COPY): Check Dockerfile for explicit COPY statements. If found, update: Old pattern: COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/controller/ internal/controller/ Option 1 - Simplify (recommended): COPY . . Ensure .dockerignore has: ** !**/*.go **/*_test.go !go.mod !go.sum Option 2 - Update explicit paths: COPY cmd/ cmd/ COPY api/ api/ COPY internal/ internal/ STEP 5 - Verify reorganization: - Run: go mod tidy - Run: make generate - Run: make manifests - Run: make build - Run: make test If errors, fix import paths. Success: new layout, make build succeeds, make test passes, project functional ``` ## What This Does The AI will: 1. **Move files** to new layout (controllers/ to internal/controller/, webhooks to internal/webhook/, main.go to cmd/) 2. **Fix import paths** in all files after moves 3. **Verify** the reorganized project builds and tests pass After this step, your project uses the new layout (same code, new locations), making migration much simpler! ## Next Steps After AI reorganizes: 1. Verify: `make build && make test` (in current project) 2. If successful, backup and proceed to [Step 2: Discovery CLI Commands](./discovery-commands.md) 3. If errors, review and fix before proceeding ================================================ FILE: docs/book/src/migrations.md ================================================ # Migrations Upgrading your Kubebuilder project to the latest version ensures you benefit from new features, bug fixes, and ecosystem improvements. It is recommended to keep your project aligned with ecosystem changes. Migration may involve updating to a newer plugin version (e.g., from `go.kubebuilder.io/v3` in release 3.x to `go.kubebuilder.io/v4` in release 4.x) or updating the scaffold produced by the same plugin across CLI releases (e.g., from `v4.9.0` to `v4.10.1`). Kubebuilder provides multiple migration paths to suit your workflow. Choose the approach that best fits your needs. ## Migration Options > [!TIP] > To reduce effort, we recommend enabling the [AutoUpdate Plugin][autoupdate-v1-alpha] (GitHub Actions). You can also run [alpha update](./reference/commands/alpha_update.md) locally—both use the same update logic. Use the other options mainly for older projects that do not have `cliVersion` in the `PROJECT` file as a one-time step to reach a supported version; after that, use these workflows for future updates (older versions cannot use these automation features). ### **(Recommended)** AutoUpdate/GitHub Action: Get Notified of New Kubebuilder Releases via Issues with a PR Link to Review and Upgrade The [AutoUpdate Plugin][autoupdate-v1-alpha] scaffolds an action that automatically monitors for new Kubebuilder releases and opens a GitHub Issue with a Pull Request compare link when updates are available. This is ideal for keeping your project up to date with minimal manual work. This plugin provides a mechanism similar to Dependabot for GitHub, offering continuous updates with AI assistance for projects that follow the standard scaffold. ```bash kubebuilder edit --plugins="autoupdate/v1-alpha" ``` See the [AutoUpdate Plugin documentation][autoupdate-v1-alpha] for complete details. ### **(Recommended)** Use `alpha update` to Upgrade Without Losing Customisations (Logic Behind AutoUpdate/GitHub Action) If you prefer to run updates locally instead of relying on GitHub Actions, you can use the same logic as the [AutoUpdate Plugin][autoupdate-v1-alpha] directly from your command line. ```shell kubebuilder alpha update ``` This command uses the same underlying mechanism as the AutoUpdate Plugin. You can migrate your project, resolve any conflicts if needed, and then push a Pull Request from your local environment. See the [`alpha update` command reference](./reference/commands/alpha_update.md) for all options and flags. ### Regenerate with Help and Merge Manually The `kubebuilder alpha generate` command re-scaffolds your entire project based on your `PROJECT` file configuration. You can then manually compare and merge your custom code. For example, you can use it to regenerate your project after upgrading the Kubebuilder CLI version and then, manually use an IDE or `git diff` to compare and merge changes by hand into your existing codebase to ensure that all your changes are applied in a new scaffold. This approach is useful for projects that heavily customize the scaffold or when other migration methods aren't available. You might need to use this method only once to establish a baseline for future automated updates. ```shell kubebuilder alpha generate ``` See the [`alpha generate` command reference](./reference/commands/alpha_generate.md) for details. ### Fully Manual Migration For complete control, you can manually migrate by creating a new project with the latest Kubebuilder version and porting your code over. In this process, you will run all commands from scratch to create a new project, APIs, controllers, webhooks, and other resources. Then, manually copy your business logic and customizations from your old project to the new one. To streamline this one-time migration, [AI Migration Helpers](./migration/ai-helpers.md) have been added to automate repetitive tasks. See the [Manual Migration Process Guide](./migration/manual-process.md) for a complete step-by-step walkthrough with AI helpers. [project-config]: ./reference/project-config.md [basic-project-doc]: ./cronjob-tutorial/basic-project.md [autoupdate-v1-alpha]: ./plugins/available/autoupdate-v1-alpha.md ================================================ FILE: docs/book/src/multiversion-tutorial/api-changes.md ================================================ # Changing things up A fairly common change in a Kubernetes API is to take some data that used to be unstructured or stored in some special string format, and change it to structured data. Our `schedule` field fits the bill quite nicely for this -- right now, in `v1`, our schedules look like ```yaml schedule: "*/1 * * * *" ``` That's a pretty textbook example of a special string format (it's also pretty unreadable unless you're a Unix sysadmin). Let's make it a bit more structured. According to our [CronJob code][cronjob-sched-code], we support "standard" Cron format. In Kubernetes, **all versions must be safely round-tripable through each other**. This means that if we convert from version 1 to version 2, and then back to version 1, we must not lose information. Thus, any change we make to our API must be compatible with whatever we supported in v1, and also need to make sure anything we add in v2 is supported in v1. In some cases, this means we need to add new fields to v1, but in our case, we won't have to, since we're not adding new functionality. Keeping all that in mind, let's convert our example above to be slightly more structured: ```yaml schedule: minute: */1 ``` Now, at least, we've got labels for each of our fields, but we can still easily support all the different syntax for each field. We'll need a new API version for this change. Let's call it v2: ```shell kubebuilder create api --group batch --version v2 --kind CronJob ``` Press `y` for "Create Resource" and `n` for "Create Controller". Now, let's copy over our existing types, and make the change: {{#literatego ./testdata/project/api/v2/cronjob_types.go}} ## Storage Versions {{#literatego ./testdata/project/api/v1/cronjob_types.go}} Now that we've got our types in place, we'll need to set up conversion... [cronjob-sched-code]: ./multiversion-tutorial/testdata/project/api/v2/cronjob_types.go "CronJob Code" ================================================ FILE: docs/book/src/multiversion-tutorial/conversion-concepts.md ================================================ # Hubs, spokes, and other wheel metaphors Since we now have two different versions, and users can request either version, we'll have to define a way to convert between our version. For CRDs, this is done using a webhook, similar to the defaulting and validating webhooks we [defined in the base tutorial](/cronjob-tutorial/webhook-implementation.md). Like before, controller-runtime will help us wire together the nitty-gritty bits, we just have to implement the actual conversion. Before we do that, though, we'll need to understand how controller-runtime thinks about versions. Namely: ## Complete graphs are insufficiently nautical A simple approach to defining conversion might be to define conversion functions to convert between each of our versions. Then, whenever we need to convert, we'd look up the appropriate function, and call it to run the conversion. This works fine when we just have two versions, but what if we had 4 types? 8 types? That'd be a lot of conversion functions. Instead, controller-runtime models conversion in terms of a "hub and spoke" model -- we mark one version as the "hub", and all other versions just define conversion to and from the hub:
{{#include ./complete-graph-8.svg}}
becomes
{{#include ./hub-spoke-graph.svg}}
Then, if we have to convert between two non-hub versions, we first convert to the hub version, and then to our desired version:
{{#include ./conversion-diagram.svg}}
This cuts down on the number of conversion functions that we have to define, and is modeled off of what Kubernetes does internally. ## What does that have to do with Webhooks? When API clients, like kubectl or your controller, request a particular version of your resource, the Kubernetes API server needs to return a result that's of that version. However, that version might not match the version stored by the API server. In that case, the API server needs to know how to convert between the desired version and the stored version. Since the conversions aren't built in for CRDs, the Kubernetes API server calls out to a webhook to do the conversion instead. For Kubebuilder, this webhook is implemented by controller-runtime, and performs the hub-and-spoke conversions that we discussed above. Now that we have the model for conversion down pat, we can actually implement our conversions. ================================================ FILE: docs/book/src/multiversion-tutorial/conversion.md ================================================ # Implementing conversion With our model for conversion in place, it's time to actually implement the conversion functions. We'll create a conversion webhook for our CronJob API version `v1` (Hub) to Spoke our CronJob API version `v2` see: ```go kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2 ``` The above command will generate the `cronjob_conversion.go` next to our `cronjob_types.go` file, to avoid cluttering up our main types file with extra functions. ## Hub... First, we'll implement the hub. We'll choose the v1 version as the hub: {{#literatego ./testdata/project/api/v1/cronjob_conversion.go}} ## ... and Spokes Then, we'll implement our spoke, the v2 version: {{#literatego ./testdata/project/api/v2/cronjob_conversion.go}} Now that we've got our conversions in place, all that we need to do is wire up our main to serve the webhook! ================================================ FILE: docs/book/src/multiversion-tutorial/deployment.md ================================================ # Deployment and Testing Before we can test out our conversion, we'll need to enable them in our CRD: Kubebuilder generates Kubernetes manifests under the `config` directory with webhook bits disabled. To enable them, we need to: - Enable `patches/webhook_in_.yaml` and `patches/cainjection_in_.yaml` in `config/crd/kustomization.yaml` file. - Enable `../certmanager` and `../webhook` directories under the `bases` section in `config/default/kustomization.yaml` file. - Enable all the vars under the `CERTMANAGER` section in `config/default/kustomization.yaml` file. Additionally, if present in our Makefile, we'll need to set the `CRD_OPTIONS` variable to just `"crd"`, removing the `trivialVersions` option (this ensures that we actually [generate validation for each version][ref-multiver], instead of telling Kubernetes that they're the same): ```makefile CRD_OPTIONS ?= "crd" ``` Now we have all our code changes and manifests in place, so let's deploy it to the cluster and test it out. You'll need [cert-manager](../cronjob-tutorial/cert-manager.md) installed (version `0.9.0+`) unless you've got some other certificate management solution. The Kubebuilder team has tested the instructions in this tutorial with [0.9.0-alpha.0](https://github.com/cert-manager/cert-manager/releases/tag/v0.9.0-alpha.0) release. Once all our ducks are in a row with certificates, we can run `make install deploy` (as normal) to deploy all the bits (CRD, controller-manager deployment) onto the cluster. ## Testing Once all of the bits are up and running on the cluster with conversion enabled, we can test out our conversion by requesting different versions. We'll make a v2 version based on our v1 version (put it under `config/samples`) ```yaml {{#include ./testdata/project/config/samples/batch_v2_cronjob.yaml}} ``` Then, we can create it on the cluster: ```shell kubectl apply -f config/samples/batch_v2_cronjob.yaml ``` If we've done everything correctly, it should create successfully, and we should be able to fetch it using both the v2 resource ```shell kubectl get cronjobs.v2.batch.tutorial.kubebuilder.io -o yaml ``` ```yaml {{#include ./testdata/project/config/samples/batch_v2_cronjob.yaml}} ``` and the v1 resource ```shell kubectl get cronjobs.v1.batch.tutorial.kubebuilder.io -o yaml ``` ```yaml {{#include ./testdata/project/config/samples/batch_v1_cronjob.yaml}} ``` Both should be filled out, and look equivalent to our v2 and v1 samples, respectively. Notice that each has a different API version. Finally, if we wait a bit, we should notice that our CronJob continues to reconcile, even though our controller is written against our v1 API version. [ref-multiver]: /reference/generating-crd.md#multiple-versions "Generating CRDs: Multiple Versions" [crd-version-pref]: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#version-priority "Versions in CustomResourceDefinitions" ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.custom-gcl.yml ================================================ # This file configures golangci-lint with module plugins. # When you run 'make lint', it will automatically build a custom golangci-lint binary # with all the plugins listed below. # # See: https://golangci-lint.run/plugins/module-plugins/ version: v2.8.0 plugins: # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs) - module: "sigs.k8s.io/logtools" import: "sigs.k8s.io/logtools/logcheck/gclplugin" version: latest ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.devcontainer/devcontainer.json ================================================ { "name": "Kubebuilder DevContainer", "image": "golang:1.25", "features": { "ghcr.io/devcontainers/features/docker-in-docker:2": { "moby": false, "dockerDefaultAddressPool": "base=172.30.0.0/16,size=24" }, "ghcr.io/devcontainers/features/git:1": {}, "ghcr.io/devcontainers/features/common-utils:2": { "upgradePackages": true } }, "runArgs": ["--privileged", "--init"], "customizations": { "vscode": { "settings": { "terminal.integrated.shell.linux": "/bin/bash" }, "extensions": [ "ms-kubernetes-tools.vscode-kubernetes-tools", "ms-azuretools.vscode-docker" ] } }, "remoteEnv": { "GO111MODULE": "on" }, "onCreateCommand": "bash .devcontainer/post-install.sh" } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.devcontainer/post-install.sh ================================================ #!/bin/bash set -euo pipefail echo "====================================" echo "Kubebuilder DevContainer Setup" echo "====================================" # Verify running as root (required for installing to /usr/local/bin and /etc) if [ "$(id -u)" -ne 0 ]; then echo "ERROR: This script must be run as root" exit 1 fi echo "" echo "Detecting system architecture..." # Detect architecture using uname MACHINE=$(uname -m) case "${MACHINE}" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; *) echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64" ARCH="amd64" ;; esac echo "Architecture: ${ARCH}" echo "" echo "------------------------------------" echo "Setting up bash completion..." echo "------------------------------------" BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions" # Enable bash-completion in root's .bashrc (devcontainer runs as root) if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc echo "Added bash-completion to .bashrc" fi echo "" echo "------------------------------------" echo "Installing development tools..." echo "------------------------------------" # Install kind if ! command -v kind &> /dev/null; then echo "Installing kind..." curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}" chmod +x /usr/local/bin/kind echo "kind installed successfully" fi # Generate kind bash completion if command -v kind &> /dev/null; then if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then echo "kind completion installed" else echo "WARNING: Failed to generate kind completion" fi fi # Install kubebuilder if ! command -v kubebuilder &> /dev/null; then echo "Installing kubebuilder..." curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}" chmod +x /usr/local/bin/kubebuilder echo "kubebuilder installed successfully" fi # Generate kubebuilder bash completion if command -v kubebuilder &> /dev/null; then if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then echo "kubebuilder completion installed" else echo "WARNING: Failed to generate kubebuilder completion" fi fi # Install kubectl if ! command -v kubectl &> /dev/null; then echo "Installing kubectl..." KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt) curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl" chmod +x /usr/local/bin/kubectl echo "kubectl installed successfully" fi # Generate kubectl bash completion if command -v kubectl &> /dev/null; then if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then echo "kubectl completion installed" else echo "WARNING: Failed to generate kubectl completion" fi fi # Generate Docker bash completion if command -v docker &> /dev/null; then if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then echo "docker completion installed" else echo "WARNING: Failed to generate docker completion" fi fi echo "" echo "------------------------------------" echo "Configuring Docker environment..." echo "------------------------------------" # Wait for Docker to be ready echo "Waiting for Docker to be ready..." for i in {1..30}; do if docker info >/dev/null 2>&1; then echo "Docker is ready" break fi if [ "$i" -eq 30 ]; then echo "WARNING: Docker not ready after 30s" fi sleep 1 done # Create kind network (ignore if already exists) if ! docker network inspect kind >/dev/null 2>&1; then if docker network create kind >/dev/null 2>&1; then echo "Created kind network" else echo "WARNING: Failed to create kind network (may already exist)" fi fi echo "" echo "------------------------------------" echo "Verifying installations..." echo "------------------------------------" kind version kubebuilder version kubectl version --client docker --version go version echo "" echo "====================================" echo "DevContainer ready!" echo "====================================" echo "All development tools installed successfully." echo "You can now start building Kubernetes operators." ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.dockerignore ================================================ # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file # Ignore everything by default and re-include only needed files ** # Re-include Go source files (but not *_test.go) !**/*.go **/*_test.go # Re-include Go module files !go.mod !go.sum ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.github/workflows/lint.yml ================================================ name: Lint on: push: pull_request: jobs: lint: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Check linter configuration run: make lint-config - name: Run linter run: make lint ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.github/workflows/test-chart.yml ================================================ name: Test Chart on: push: pull_request: jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Create kind cluster run: kind create cluster - name: Prepare project run: | go mod tidy make docker-build IMG=controller:latest kind load docker-image controller:latest - name: Install Helm run: make install-helm - name: Lint Helm Chart run: | helm lint ./dist/chart - name: Install cert-manager via Helm (wait for readiness) run: | helm repo add jetstack https://charts.jetstack.io helm repo update helm install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true \ --wait \ --timeout 300s # TODO: Uncomment if Prometheus is enabled # - name: Install Prometheus Operator CRDs # run: | # helm repo add prometheus-community https://prometheus-community.github.io/helm-charts # helm repo update # helm install prometheus-crds prometheus-community/prometheus-operator-crds - name: Deploy manager via Helm run: | make helm-deploy IMG=project:v0.1.0 - name: Check Helm release status run: | make helm-status ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.github/workflows/test-e2e.yml ================================================ name: E2E Tests on: push: pull_request: jobs: test-e2e: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Install the latest version of kind run: | curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH) chmod +x ./kind sudo mv ./kind /usr/local/bin/kind - name: Verify kind installation run: kind version - name: Running Test e2e run: | go mod tidy make test-e2e ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.github/workflows/test.yml ================================================ name: Tests on: push: pull_request: jobs: test: name: Run on Ubuntu runs-on: ubuntu-latest steps: - name: Clone the code uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Running Tests run: | go mod tidy make test ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib bin/* Dockerfile.cross # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Go workspace file go.work # Kubernetes Generated files - skip generated files, except for vendored files !vendor/**/zz_generated.* # editor and IDE paraphernalia .idea .vscode *.swp *.swo *~ # Kubeconfig might contain secrets *.kubeconfig ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/.golangci.yml ================================================ version: "2" run: allow-parallel-runners: true linters: default: none enable: - copyloopvar - dupl - errcheck - ginkgolinter - goconst - gocyclo - govet - ineffassign - lll - modernize - misspell - nakedret - prealloc - revive - staticcheck - unconvert - unparam - unused - logcheck settings: custom: logcheck: type: "module" description: Checks Go logging calls for Kubernetes logging conventions. revive: rules: - name: comment-spacings - name: import-shadowing modernize: disable: - omitzero exclusions: generated: lax rules: - linters: - lll path: api/* - linters: - dupl - lll path: internal/* paths: - third_party$ - builtin$ - examples$ formatters: enable: - gofmt - goimports exclusions: generated: lax paths: - third_party$ - builtin$ - examples$ ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/AGENTS.md ================================================ # project - AI Agent Guide ## Project Structure **Single-group layout (default):** ``` cmd/main.go Manager entry (registers controllers/webhooks) api//*_types.go CRD schemas (+kubebuilder markers) api//zz_generated.* Auto-generated (DO NOT EDIT) internal/controller/* Reconciliation logic internal/webhook/* Validation/defaulting (if present) config/crd/bases/* Generated CRDs (DO NOT EDIT) config/rbac/role.yaml Generated RBAC (DO NOT EDIT) config/samples/* Example CRs (edit these) Makefile Build/test/deploy commands PROJECT Kubebuilder metadata Auto-generated (DO NOT EDIT) ``` **Multi-group layout** (for projects with multiple API groups): ``` api///*_types.go CRD schemas by group internal/controller//* Controllers by group internal/webhook///* Webhooks by group and version (if present) ``` Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`. **To convert to multi-group layout:** 1. Run: `kubebuilder edit --multigroup=true` 2. Move APIs: `mkdir -p api/ && mv api/ api//` 3. Move controllers: `mkdir -p internal/controller/ && mv internal/controller/*.go internal/controller//` 4. Move webhooks (if present): `mkdir -p internal/webhook/ && mv internal/webhook/ internal/webhook//` 5. Update import paths in all files 6. Fix `path` in `PROJECT` file for each resource 7. Update test suite CRD paths (add one more `..` to relative paths) ## Critical Rules ### Never Edit These (Auto-Generated) - `config/crd/bases/*.yaml` - from `make manifests` - `config/rbac/role.yaml` - from `make manifests` - `config/webhook/manifests.yaml` - from `make manifests` - `**/zz_generated.*.go` - from `make generate` - `PROJECT` - from `kubebuilder [OPTIONS]` ### Never Remove Scaffold Markers Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers. ### Keep Project Structure Do not move files around. The CLI expects files in specific locations. ### Always Use CLI Commands Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually. ### E2E Tests Require an Isolated Kind Cluster The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI). Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster). ## After Making Changes **After editing `*_types.go` or markers:** ``` make manifests # Regenerate CRDs/RBAC from markers make generate # Regenerate DeepCopy methods ``` **After editing `*.go` files:** ``` make lint-fix # Auto-fix code style make test # Run unit tests ``` ## CLI Commands Cheat Sheet ### Create API (your own types) ```bash kubebuilder create api --group --version --kind ``` ### Deploy Image Plugin (scaffold to deploy/manage ANY container image) Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.): ```bash # Example: deploying memcached kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \ --image=memcached:alpine \ --plugins=deploy-image.go.kubebuilder.io/v1-alpha ``` Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation. ### Create Webhooks ```bash # Validation + defaulting kubebuilder create webhook --group --version --kind \ --defaulting --programmatic-validation # Conversion webhook (for multi-version APIs) kubebuilder create webhook --group --version v1 --kind \ --conversion --spoke v2 ``` ### Controller for Core Kubernetes Types ```bash # Watch Pods kubebuilder create api --group core --version v1 --kind Pod \ --controller=true --resource=false # Watch Deployments kubebuilder create api --group apps --version v1 --kind Deployment \ --controller=true --resource=false ``` ### Controller for External Types (e.g., from other operators) Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.): ```bash # Example: watching cert-manager Certificate resources kubebuilder create api \ --group cert-manager --version v1 --kind Certificate \ --controller=true --resource=false \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=io \ --external-api-module=github.com/cert-manager/cert-manager ``` **Note:** Use `--external-api-module=@` only if you need a specific version. Otherwise, omit `@` to use what's in go.mod. ### Webhook for External Types ```bash # Example: validating external resources kubebuilder create webhook \ --group cert-manager --version v1 --kind Issuer \ --defaulting \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=io \ --external-api-module=github.com/cert-manager/cert-manager ``` ## Testing & Development ```bash make test # Run unit tests (uses envtest: real K8s API + etcd) make run # Run locally (uses current kubeconfig context) ``` Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup. ## Deployment Workflow ```bash # 1. Regenerate manifests make manifests generate # 2. Build & deploy export IMG=/:tag make docker-build docker-push IMG=$IMG # Or: kind load docker-image $IMG --name make deploy IMG=$IMG # 3. Test kubectl apply -k config/samples/ # 4. Debug kubectl logs -n -system deployment/-controller-manager -c manager -f ``` ### API Design **Key markers for** `api//*_types.go`: ```go // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Namespaced // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status" // On fields: // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:MaxLength=100 // +kubebuilder:validation:Pattern="^[a-z]+$" // +kubebuilder:default="value" ``` - **Use** `metav1.Condition` for status (not custom string fields) - **Use predefined types**: `metav1.Time` instead of `string` for dates - **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`) ### Controller Design **RBAC markers in** `internal/controller/*_controller.go`: ```go // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete ``` **Implementation rules:** - **Idempotent reconciliation**: Safe to run multiple times - **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts - **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)` - **Owner references**: Enable automatic garbage collection (`SetControllerReference`) - **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter` - **Finalizers**: Clean up external resources (buckets, VMs, DNS entries) ### Logging **Follow Kubernetes logging message style guidelines:** - Start from a capital letter - Do not end the message with a period - Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`) - Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"` - Specify object type: `"Deleted Pod"` not `"Deleted"` - Balanced key-value pairs ```go log.Info("Starting reconciliation") log.Info("Created Deployment", "name", deploy.Name) log.Error(err, "Failed to create Pod", "name", name) ``` **Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines ### Webhooks - **Create all types together**: `--defaulting --programmatic-validation --conversion` - **When`--force`is used**: Backup custom logic first, then restore after scaffolding - **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`) - Hub version: Usually oldest stable version (v1) - Spoke versions: Newer versions that convert to/from hub (v2, v3) - Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke) ### Learning from Examples The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation: ```bash kubebuilder create api --group example --version v1alpha1 --kind MyApp \ --image= --plugins=deploy-image.go.kubebuilder.io/v1-alpha ``` Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation. ## Distribution Options ### Option 1: YAML Bundle (Kustomize) ```bash # Generate dist/install.yaml from Kustomize manifests make build-installer IMG=/:tag ``` **Key points:** - The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment) - Commit this file to your repository for easy distribution - Users only need `kubectl` to install (no additional tools required) **Example:** Users install with a single command: ```bash kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml ``` ### Option 2: Helm Chart ```bash kubebuilder edit --plugins=helm/v2-alpha # Generates dist/chart/ (default) kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts # Generates charts/chart/ ``` **For development:** ```bash make helm-deploy IMG=/: # Deploy manager via Helm make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..." # Deploy with custom values make helm-status # Show release status make helm-uninstall # Remove release make helm-history # View release history make helm-rollback # Rollback to previous version ``` **For end users/production:** ```bash helm install my-release .//chart/ --namespace --create-namespace ``` **Important:** If you add webhooks or modify manifests after initial chart generation: 1. Backup any customizations in `/chart/values.yaml` and `/chart/manager/manager.yaml` 2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized) 3. Manually restore your custom values from the backup ### Publish Container Image ```bash export IMG=/: make docker-build docker-push IMG=$IMG ``` ## References ### Essential Reading - **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide) - **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions) - **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.) - **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels) ### API Design & Implementation - **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md - **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ - **Markers Reference**: https://book.kubebuilder.io/reference/markers.html ### Tools & Libraries - **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime - **controller-tools**: https://github.com/kubernetes-sigs/controller-tools - **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/Dockerfile ================================================ # Build the manager binary FROM golang:1.25 AS builder ARG TARGETOS ARG TARGETARCH WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the Go source (relies on .dockerignore to filter) COPY . . # Build # the GOARCH has no default value to allow the binary to be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 ENTRYPOINT ["/manager"] ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/Makefile ================================================ # Image URL to use all building/pushing image targets IMG ?= controller:latest # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin else GOBIN=$(shell go env GOBIN) endif # CONTAINER_TOOL defines the container tool to be used for building images. # Be aware that the target commands are only tested with Docker which is # scaffolded by default. However, you might want to replace it to use other # tools. (i.e. podman) CONTAINER_TOOL ?= docker # Setting SHELL to bash allows bash commands to be executed by recipes. # Options are set to exit when a recipe line exits non-zero or a piped command fails. SHELL = /usr/bin/env bash -o pipefail .SHELLFLAGS = -ec .PHONY: all all: build ##@ General # The help target prints out all targets with their descriptions organized # beneath their categories. The categories are represented by '##@' and the # target descriptions by '##'. The awk command is responsible for reading the # entire set of makefiles included in this invocation, looking for lines of the # file as xyz: ## something, and then pretty-format the target and help. Then, # if there's a line with ##@ something, that gets pretty-printed as a category. # More info on the usage of ANSI control characters for terminal formatting: # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters # More info on the awk command: # http://linuxcommand.org/lc3_adv_awk.php .PHONY: help help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) ##@ Development .PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. # Note that the option maxDescLen=0 was added in the default scaffold in order to sort out the issue # Too long: must have at most 262144 bytes. By using kubectl apply to create / update resources an annotation # is created by K8s API to store the latest version of the resource ( kubectl.kubernetes.io/last-applied-configuration). # However, it has a size limit and if the CRD is too big with so many long descriptions as this one it will cause the failure. "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. go fmt ./... .PHONY: vet vet: ## Run go vet against code. go vet ./... .PHONY: test test: manifests generate fmt vet setup-envtest ## Run tests. KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out # TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'. # The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally. # CertManager is installed by default; skip with: # - CERT_MANAGER_INSTALL_SKIP=true KIND_CLUSTER ?= project-test-e2e .PHONY: setup-test-e2e setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist @command -v $(KIND) >/dev/null 2>&1 || { \ echo "Kind is not installed. Please install Kind manually."; \ exit 1; \ } @case "$$($(KIND) get clusters)" in \ *"$(KIND_CLUSTER)"*) \ echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \ *) \ echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \ $(KIND) create cluster --name $(KIND_CLUSTER) ;; \ esac .PHONY: test-e2e test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind. KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v $(MAKE) cleanup-test-e2e .PHONY: cleanup-test-e2e cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests @$(KIND) delete cluster --name $(KIND_CLUSTER) .PHONY: lint lint: golangci-lint ## Run golangci-lint linter "$(GOLANGCI_LINT)" run .PHONY: lint-fix lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes "$(GOLANGCI_LINT)" run --fix .PHONY: lint-config lint-config: golangci-lint ## Verify golangci-lint linter configuration "$(GOLANGCI_LINT)" config verify ##@ Build .PHONY: build build: manifests generate fmt vet ## Build manager binary. go build -o bin/manager cmd/main.go .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. go run ./cmd/main.go # If you wish to build the manager image targeting other platforms you can use the --platform flag. # (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it. # More info: https://docs.docker.com/develop/develop-images/build_enhancements/ .PHONY: docker-build docker-build: ## Build docker image with the manager. $(CONTAINER_TOOL) build -t ${IMG} . .PHONY: docker-push docker-push: ## Push docker image with the manager. $(CONTAINER_TOOL) push ${IMG} # PLATFORMS defines the target platforms for the manager image be built to provide support to multiple # architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to: # - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/ # - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/ # - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail) # To adequately provide solutions that are compatible with multiple platforms, you should consider using this option. PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le .PHONY: docker-buildx docker-buildx: ## Build and push docker image for the manager for cross-platform support # copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross - $(CONTAINER_TOOL) buildx create --name project-builder $(CONTAINER_TOOL) buildx use project-builder - $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross . - $(CONTAINER_TOOL) buildx rm project-builder rm Dockerfile.cross .PHONY: build-installer build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment. mkdir -p dist cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} "$(KUSTOMIZE)" build config/default > dist/install.yaml ##@ Deployment ifndef ignore-not-found ignore-not-found = false endif .PHONY: install install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config. @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi .PHONY: uninstall uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. @out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \ if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG} "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f - .PHONY: undeploy undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. "$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f - ##@ Dependencies ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p "$(LOCALBIN)" ## Tool Binaries KUBECTL ?= kubectl KIND ?= kind KUSTOMIZE ?= $(LOCALBIN)/kustomize CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest GOLANGCI_LINT = $(LOCALBIN)/golangci-lint ## Tool Versions KUSTOMIZE_VERSION ?= v5.8.1 CONTROLLER_TOOLS_VERSION ?= v0.20.1 #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \ [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \ printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \ [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \ printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/') GOLANGCI_LINT_VERSION ?= v2.8.0 .PHONY: kustomize kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary. $(KUSTOMIZE): $(LOCALBIN) $(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION)) .PHONY: controller-gen controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary. $(CONTROLLER_GEN): $(LOCALBIN) $(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION)) .PHONY: setup-envtest setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." @"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \ echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ exit 1; \ } .PHONY: envtest envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. $(ENVTEST): $(LOCALBIN) $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) .PHONY: golangci-lint golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) @test -f .custom-gcl.yml && { \ echo "Building custom golangci-lint with plugins..." && \ $(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \ mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \ } || true # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed # $3 - specific version of package define go-install-tool @[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ set -e; \ package=$(2)@$(3) ;\ echo "Downloading $${package}" ;\ rm -f "$(1)" ;\ GOBIN="$(LOCALBIN)" go install $${package} ;\ mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\ } ;\ ln -sf "$$(realpath "$(1)-$(3)")" "$(1)" endef define gomodver $(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null) endef ##@ Helm Deployment ## Helm binary to use for deploying the chart HELM ?= helm ## Namespace to deploy the Helm release HELM_NAMESPACE ?= project-system ## Name of the Helm release HELM_RELEASE ?= project ## Path to the Helm chart directory HELM_CHART_DIR ?= dist/chart ## Additional arguments to pass to helm commands HELM_EXTRA_ARGS ?= .PHONY: install-helm install-helm: ## Install the latest version of Helm. @command -v $(HELM) >/dev/null 2>&1 || { \ echo "Installing Helm..." && \ curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \ } .PHONY: helm-deploy helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG. $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \ --namespace $(HELM_NAMESPACE) \ --create-namespace \ --set manager.image.repository=$${IMG%:*} \ --set manager.image.tag=$${IMG##*:} \ --wait \ --timeout 5m \ $(HELM_EXTRA_ARGS) .PHONY: helm-uninstall helm-uninstall: ## Uninstall the Helm release from the K8s cluster. $(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-status helm-status: ## Show Helm release status. $(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-history helm-history: ## Show Helm release history. $(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) .PHONY: helm-rollback helm-rollback: ## Rollback to previous Helm release. $(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE) ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/PROJECT ================================================ # Code generated by tool. DO NOT EDIT. # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html cliVersion: (devel) domain: tutorial.kubebuilder.io layout: - go.kubebuilder.io/v4 plugins: helm.kubebuilder.io/v2-alpha: manifests: dist/install.yaml output: dist projectName: project repo: tutorial.kubebuilder.io/project resources: - api: crdVersion: v1 namespaced: true controller: true domain: tutorial.kubebuilder.io group: batch kind: CronJob path: tutorial.kubebuilder.io/project/api/v1 version: v1 webhooks: conversion: true defaulting: true spoke: - v2 validation: true webhookVersion: v1 - api: crdVersion: v1 namespaced: true domain: tutorial.kubebuilder.io group: batch kind: CronJob path: tutorial.kubebuilder.io/project/api/v2 version: v2 webhooks: defaulting: true validation: true webhookVersion: v1 version: "3" ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/README.md ================================================ # project // TODO(user): Add simple overview of use/purpose ## Description // TODO(user): An in-depth paragraph about your project and overview of use ## Getting Started ### Prerequisites - go version v1.24.6+ - docker version 17.03+. - kubectl version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. ### To Deploy on the cluster **Build and push your image to the location specified by `IMG`:** ```sh make docker-build docker-push IMG=/project:tag ``` **NOTE:** This image ought to be published in the personal registry you specified. And it is required to have access to pull the image from the working environment. Make sure you have the proper permission to the registry if the above commands don’t work. **Install the CRDs into the cluster:** ```sh make install ``` **Deploy the Manager to the cluster with the image specified by `IMG`:** ```sh make deploy IMG=/project:tag ``` > **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin privileges or be logged in as admin. **Create instances of your solution** You can apply the samples (examples) from the config/sample: ```sh kubectl apply -k config/samples/ ``` >**NOTE**: Ensure that the samples has default values to test it out. ### To Uninstall **Delete the instances (CRs) from the cluster:** ```sh kubectl delete -k config/samples/ ``` **Delete the APIs(CRDs) from the cluster:** ```sh make uninstall ``` **UnDeploy the controller from the cluster:** ```sh make undeploy ``` ## Project Distribution Following the options to release and provide this solution to the users. ### By providing a bundle with all YAML files 1. Build the installer for the image built and published in the registry: ```sh make build-installer IMG=/project:tag ``` **NOTE:** The makefile target mentioned above generates an 'install.yaml' file in the dist directory. This file contains all the resources built with Kustomize, which are necessary to install this project without its dependencies. 2. Using the installer Users can just run 'kubectl apply -f ' to install the project, i.e.: ```sh kubectl apply -f https://raw.githubusercontent.com//project//dist/install.yaml ``` ### By providing a Helm Chart 1. Build the chart using the optional helm plugin ```sh kubebuilder edit --plugins=helm/v2-alpha ``` 2. See that a chart was generated under 'dist/chart', and users can obtain this solution from there. **NOTE:** If you change the project, you need to update the Helm Chart using the same command above to sync the latest changes. Furthermore, if you create webhooks, you need to use the above command with the '--force' flag and manually ensure that any custom configuration previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml' is manually re-applied afterwards. ## Contributing // TODO(user): Add detailed information on how you would like others to contribute to this project **NOTE:** Run `make help` for more information on all potential `make` targets More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) ## License Copyright 2026 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: docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_conversion.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package v1 /* Implementing the hub method is pretty easy -- we just have to add an empty method called `Hub()`to serve as a [marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). We could also just put this inline in our cronjob_types.go file. */ // Hub marks this type as a conversion hub. func (*CronJob) Hub() {} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/api/v1/cronjob_types.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* */ package v1 /* */ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // +kubebuilder:docs-gen:collapse=Imports // CronJobSpec defines the desired state of CronJob type CronJobSpec struct { // schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. // +kubebuilder:validation:MinLength=0 // +required Schedule string `json:"schedule"` // startingDeadlineSeconds defines in seconds for starting the job if it misses scheduled // time for any reason. Missed jobs executions will be counted as failed ones. // +optional // +kubebuilder:validation:Minimum=0 StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"` // concurrencyPolicy specifies how to treat concurrent executions of a Job. // Valid values are: // - "Allow" (default): allows CronJobs to run concurrently; // - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; // - "Replace": cancels currently running job and replaces it with a new one // +optional // +kubebuilder:default:=Allow ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"` // suspend tells the controller to suspend subsequent executions, it does // not apply to already started executions. Defaults to false. // +optional Suspend *bool `json:"suspend,omitempty"` // jobTemplate defines the job that will be created when executing a CronJob. // +required JobTemplate batchv1.JobTemplateSpec `json:"jobTemplate"` // successfulJobsHistoryLimit defines the number of successful finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"` // failedJobsHistoryLimit defines the number of failed finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"` } // ConcurrencyPolicy describes how the job will be handled. // Only one of the following concurrent policies may be specified. // If none of the following policies is specified, the default one // is AllowConcurrent. // +kubebuilder:validation:Enum=Allow;Forbid;Replace type ConcurrencyPolicy string const ( // AllowConcurrent allows CronJobs to run concurrently. AllowConcurrent ConcurrencyPolicy = "Allow" // ForbidConcurrent forbids concurrent runs, skipping next run if previous // hasn't finished yet. ForbidConcurrent ConcurrencyPolicy = "Forbid" // ReplaceConcurrent cancels currently running job and replaces it with a new one. ReplaceConcurrent ConcurrencyPolicy = "Replace" ) // CronJobStatus defines the observed state of CronJob. type CronJobStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // active defines a list of pointers to currently running jobs. // +optional // +listType=atomic // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 Active []corev1.ObjectReference `json:"active,omitempty"` // lastScheduleTime defines when was the last time the job was successfully scheduled. // +optional LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"` // For Kubernetes API conventions, see: // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties // conditions represent the current state of the CronJob resource. // Each condition has a unique type and reflects the status of a specific aspect of the resource. // // Standard condition types include: // - "Available": the resource is fully functional // - "Progressing": the resource is being created or updated // - "Degraded": the resource failed to reach or maintain its desired state // // The status of each condition is one of True, False, or Unknown. // +listType=map // +listMapKey=type // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:docs-gen:collapse=Remaining code from cronjob_types.go /* Since we'll have more than one version, we'll need to mark a storage version. This is the version that the Kubernetes API server uses to store our data. We'll chose the v1 version for our project. We'll use the [`+kubebuilder:storageversion`](/reference/markers/crd.md) to do this. Note that multiple versions may exist in storage if they were written before the storage version changes -- changing the storage version only affects how objects are created/updated after the change. */ // +kubebuilder:object:root=true // +kubebuilder:storageversion // +kubebuilder:subresource:status // +versionName=v1 // +kubebuilder:storageversion // CronJob is the Schema for the cronjobs API type CronJob struct { metav1.TypeMeta `json:",inline"` // metadata is a standard object metadata // +optional metav1.ObjectMeta `json:"metadata,omitzero"` // spec defines the desired state of CronJob // +required Spec CronJobSpec `json:"spec"` // status defines the observed state of CronJob // +optional Status CronJobStatus `json:"status,omitzero"` } /* */ // +kubebuilder:object:root=true // CronJobList contains a list of CronJob type CronJobList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitzero"` Items []CronJob `json:"items"` } func init() { SchemeBuilder.Register(&CronJob{}, &CronJobList{}) } // +kubebuilder:docs-gen:collapse=Remaining code from cronjob_types.go ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/api/v1/groupversion_info.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* First, we have some *package-level* markers that denote that there are Kubernetes objects in this package, and that this package represents the group `batch.tutorial.kubebuilder.io`. The `object` generator makes use of the former, while the latter is used by the CRD generator to generate the right metadata for the CRDs it creates from this package. */ // Package v1 contains API Schema definitions for the batch v1 API group. // +kubebuilder:object:generate=true // +groupName=batch.tutorial.kubebuilder.io package v1 import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) /* Then, we have the commonly useful variables that help us set up our Scheme. Since we need to use all the types in this package in our controller, it's helpful (and the convention) to have a convenient method to add all the types to some other `Scheme`. SchemeBuilder makes this easy for us. */ var ( // SchemeGroupVersion is group version used to register these objects. // This name is used by applyconfiguration generators (e.g. controller-gen). SchemeGroupVersion = schema.GroupVersion{Group: "batch.tutorial.kubebuilder.io", Version: "v1"} // GroupVersion is an alias for SchemeGroupVersion, for backward compatibility. GroupVersion = SchemeGroupVersion // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2026 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 controller-gen. DO NOT EDIT. package v1 import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJob) DeepCopyInto(out *CronJob) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJob. func (in *CronJob) DeepCopy() *CronJob { if in == nil { return nil } out := new(CronJob) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *CronJob) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobList) DeepCopyInto(out *CronJobList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]CronJob, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobList. func (in *CronJobList) DeepCopy() *CronJobList { if in == nil { return nil } out := new(CronJobList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *CronJobList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobSpec) DeepCopyInto(out *CronJobSpec) { *out = *in if in.StartingDeadlineSeconds != nil { in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds *out = new(int64) **out = **in } if in.Suspend != nil { in, out := &in.Suspend, &out.Suspend *out = new(bool) **out = **in } in.JobTemplate.DeepCopyInto(&out.JobTemplate) if in.SuccessfulJobsHistoryLimit != nil { in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit *out = new(int32) **out = **in } if in.FailedJobsHistoryLimit != nil { in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit *out = new(int32) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSpec. func (in *CronJobSpec) DeepCopy() *CronJobSpec { if in == nil { return nil } out := new(CronJobSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobStatus) DeepCopyInto(out *CronJobStatus) { *out = *in if in.Active != nil { in, out := &in.Active, &out.Active *out = make([]corev1.ObjectReference, len(*in)) copy(*out, *in) } if in.LastScheduleTime != nil { in, out := &in.LastScheduleTime, &out.LastScheduleTime *out = (*in).DeepCopy() } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobStatus. func (in *CronJobStatus) DeepCopy() *CronJobStatus { if in == nil { return nil } out := new(CronJobStatus) in.DeepCopyInto(out) return out } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_conversion.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package v2 /* For imports, we'll need the controller-runtime [`conversion`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc) package, plus the API version for our hub type (v1), and finally some of the standard packages. */ import ( "fmt" "strings" "log" "sigs.k8s.io/controller-runtime/pkg/conversion" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Imports /* Our "spoke" versions need to implement the [`Convertible`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) interface. Namely, they'll need `ConvertTo()` and `ConvertFrom()` methods to convert to/from the hub version. */ /* ConvertTo is expected to modify its argument to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ // ConvertTo converts this CronJob (v2) to the Hub version (v1). func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error { dst := dstRaw.(*batchv1.CronJob) log.Printf("ConvertTo: Converting CronJob from Spoke version v2 to Hub version v1;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) sched := src.Spec.Schedule scheduleParts := []string{"*", "*", "*", "*", "*"} if sched.Minute != nil { scheduleParts[0] = string(*sched.Minute) } if sched.Hour != nil { scheduleParts[1] = string(*sched.Hour) } if sched.DayOfMonth != nil { scheduleParts[2] = string(*sched.DayOfMonth) } if sched.Month != nil { scheduleParts[3] = string(*sched.Month) } if sched.DayOfWeek != nil { scheduleParts[4] = string(*sched.DayOfWeek) } dst.Spec.Schedule = strings.Join(scheduleParts, " ") /* The rest of the conversion is pretty rote. */ // ObjectMeta dst.ObjectMeta = src.ObjectMeta // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds dst.Spec.ConcurrencyPolicy = batchv1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit // Status dst.Status.Active = src.Status.Active dst.Status.LastScheduleTime = src.Status.LastScheduleTime return nil } // +kubebuilder:docs-gen:collapse=rote conversion /* ConvertFrom is expected to modify its receiver to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ // ConvertFrom converts the Hub version (v1) to this CronJob (v2). func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error { src := srcRaw.(*batchv1.CronJob) log.Printf("ConvertFrom: Converting CronJob from Hub version v1 to Spoke version v2;"+ "source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name) schedParts := strings.Split(src.Spec.Schedule, " ") if len(schedParts) != 5 { return fmt.Errorf("invalid schedule: not a standard 5-field schedule") } partIfNeeded := func(raw string) *CronField { if raw == "*" { return nil } part := CronField(raw) return &part } dst.Spec.Schedule.Minute = partIfNeeded(schedParts[0]) dst.Spec.Schedule.Hour = partIfNeeded(schedParts[1]) dst.Spec.Schedule.DayOfMonth = partIfNeeded(schedParts[2]) dst.Spec.Schedule.Month = partIfNeeded(schedParts[3]) dst.Spec.Schedule.DayOfWeek = partIfNeeded(schedParts[4]) /* The rest of the conversion is pretty rote. */ // ObjectMeta dst.ObjectMeta = src.ObjectMeta // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds dst.Spec.ConcurrencyPolicy = ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit // Status dst.Status.Active = src.Status.Active dst.Status.LastScheduleTime = src.Status.LastScheduleTime return nil } // +kubebuilder:docs-gen:collapse=rote conversion ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/api/v2/cronjob_types.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* Since we're in a v2 package, controller-gen will assume this is for the v2 version automatically. We could override that with the [`+versionName` marker](/reference/markers/crd.md). */ package v2 /* */ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // +kubebuilder:docs-gen:collapse=Imports /* We'll leave our spec largely unchanged, except to change the schedule field to a new type. */ // CronJobSpec defines the desired state of CronJob type CronJobSpec struct { // schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. // +required Schedule CronSchedule `json:"schedule"` /* */ // startingDeadlineSeconds defines in seconds for starting the job if it misses scheduled // time for any reason. Missed jobs executions will be counted as failed ones. // +optional // +kubebuilder:validation:Minimum=0 StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"` // concurrencyPolicy defines how to treat concurrent executions of a Job. // Valid values are: // - "Allow" (default): allows CronJobs to run concurrently; // - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; // - "Replace": cancels currently running job and replaces it with a new one // +optional // +kubebuilder:default:=Allow ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"` // suspend tells the controller to suspend subsequent executions, it does // not apply to already started executions. Defaults to false. // +optional Suspend *bool `json:"suspend,omitempty"` // jobTemplate defines the job that will be created when executing a CronJob. // +required JobTemplate batchv1.JobTemplateSpec `json:"jobTemplate"` // successfulJobsHistoryLimit defines the number of successful finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"` // failedJobsHistoryLimit defines the number of failed finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"` } // +kubebuilder:docs-gen:collapse=CronJobSpec Full Code /* Next, we'll need to define a type to hold our schedule. Based on our proposed YAML above, it'll have a field for each corresponding Cron "field". */ // describes a Cron schedule. type CronSchedule struct { // minute specifies the minutes during which the job executes. // +optional Minute *CronField `json:"minute,omitempty"` // hour specifies the hour during which the job executes. // +optional Hour *CronField `json:"hour,omitempty"` // dayOfMonth specifies the day of the month during which the job executes. // +optional DayOfMonth *CronField `json:"dayOfMonth,omitempty"` // month specifies the month during which the job executes. // +optional Month *CronField `json:"month,omitempty"` // dayOfWeek specifies the day of the week during which the job executes. // +optional DayOfWeek *CronField `json:"dayOfWeek,omitempty"` } /* Finally, we'll define a wrapper type to represent a field. We could attach additional validation to this field, but for now we'll just use it for documentation purposes. */ // represents a Cron field specifier. type CronField string /* All the other types will stay the same as before. */ // ConcurrencyPolicy describes how the job will be handled. // Only one of the following concurrent policies may be specified. // If none of the following policies is specified, the default one // is AllowConcurrent. // +kubebuilder:validation:Enum=Allow;Forbid;Replace type ConcurrencyPolicy string const ( // AllowConcurrent allows CronJobs to run concurrently. AllowConcurrent ConcurrencyPolicy = "Allow" // ForbidConcurrent forbids concurrent runs, skipping next run if previous // hasn't finished yet. ForbidConcurrent ConcurrencyPolicy = "Forbid" // ReplaceConcurrent cancels currently running job and replaces it with a new one. ReplaceConcurrent ConcurrencyPolicy = "Replace" ) // CronJobStatus defines the observed state of CronJob. type CronJobStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // active defines a list of pointers to currently running jobs. // +optional // +listType=atomic // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 Active []corev1.ObjectReference `json:"active,omitempty"` // lastScheduleTime defines the information when was the last time the job was successfully scheduled. // +optional LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"` // For Kubernetes API conventions, see: // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties // conditions represent the current state of the CronJob resource. // Each condition has a unique type and reflects the status of a specific aspect of the resource. // // Standard condition types include: // - "Available": the resource is fully functional // - "Progressing": the resource is being created or updated // - "Degraded": the resource failed to reach or maintain its desired state // // The status of each condition is one of True, False, or Unknown. // +listType=map // +listMapKey=type // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +versionName=v2 // CronJob is the Schema for the cronjobs API type CronJob struct { metav1.TypeMeta `json:",inline"` // metadata is a standard object metadata // +optional metav1.ObjectMeta `json:"metadata,omitzero"` // spec defines the desired state of CronJob // +required Spec CronJobSpec `json:"spec"` // status defines the observed state of CronJob // +optional Status CronJobStatus `json:"status,omitzero"` } // +kubebuilder:object:root=true // CronJobList contains a list of CronJob type CronJobList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitzero"` Items []CronJob `json:"items"` } func init() { SchemeBuilder.Register(&CronJob{}, &CronJobList{}) } // +kubebuilder:docs-gen:collapse=Other Types ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/api/v2/groupversion_info.go ================================================ /* Copyright 2026 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 v2 contains API Schema definitions for the batch v2 API group. // +kubebuilder:object:generate=true // +groupName=batch.tutorial.kubebuilder.io package v2 import ( "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/scheme" ) var ( // SchemeGroupVersion is group version used to register these objects. // This name is used by applyconfiguration generators (e.g. controller-gen). SchemeGroupVersion = schema.GroupVersion{Group: "batch.tutorial.kubebuilder.io", Version: "v2"} // GroupVersion is an alias for SchemeGroupVersion, for backward compatibility. GroupVersion = SchemeGroupVersion // SchemeBuilder is used to add go types to the GroupVersionKind scheme. SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} // AddToScheme adds the types in this group-version to the given scheme. AddToScheme = SchemeBuilder.AddToScheme ) ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/api/v2/zz_generated.deepcopy.go ================================================ //go:build !ignore_autogenerated /* Copyright 2026 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 controller-gen. DO NOT EDIT. package v2 import ( "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJob) DeepCopyInto(out *CronJob) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJob. func (in *CronJob) DeepCopy() *CronJob { if in == nil { return nil } out := new(CronJob) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *CronJob) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobList) DeepCopyInto(out *CronJobList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items *out = make([]CronJob, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobList. func (in *CronJobList) DeepCopy() *CronJobList { if in == nil { return nil } out := new(CronJobList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. func (in *CronJobList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } return nil } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobSpec) DeepCopyInto(out *CronJobSpec) { *out = *in in.Schedule.DeepCopyInto(&out.Schedule) if in.StartingDeadlineSeconds != nil { in, out := &in.StartingDeadlineSeconds, &out.StartingDeadlineSeconds *out = new(int64) **out = **in } if in.Suspend != nil { in, out := &in.Suspend, &out.Suspend *out = new(bool) **out = **in } in.JobTemplate.DeepCopyInto(&out.JobTemplate) if in.SuccessfulJobsHistoryLimit != nil { in, out := &in.SuccessfulJobsHistoryLimit, &out.SuccessfulJobsHistoryLimit *out = new(int32) **out = **in } if in.FailedJobsHistoryLimit != nil { in, out := &in.FailedJobsHistoryLimit, &out.FailedJobsHistoryLimit *out = new(int32) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobSpec. func (in *CronJobSpec) DeepCopy() *CronJobSpec { if in == nil { return nil } out := new(CronJobSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronJobStatus) DeepCopyInto(out *CronJobStatus) { *out = *in if in.Active != nil { in, out := &in.Active, &out.Active *out = make([]v1.ObjectReference, len(*in)) copy(*out, *in) } if in.LastScheduleTime != nil { in, out := &in.LastScheduleTime, &out.LastScheduleTime *out = (*in).DeepCopy() } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]metav1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronJobStatus. func (in *CronJobStatus) DeepCopy() *CronJobStatus { if in == nil { return nil } out := new(CronJobStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CronSchedule) DeepCopyInto(out *CronSchedule) { *out = *in if in.Minute != nil { in, out := &in.Minute, &out.Minute *out = new(CronField) **out = **in } if in.Hour != nil { in, out := &in.Hour, &out.Hour *out = new(CronField) **out = **in } if in.DayOfMonth != nil { in, out := &in.DayOfMonth, &out.DayOfMonth *out = new(CronField) **out = **in } if in.Month != nil { in, out := &in.Month, &out.Month *out = new(CronField) **out = **in } if in.DayOfWeek != nil { in, out := &in.DayOfWeek, &out.DayOfWeek *out = new(CronField) **out = **in } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CronSchedule. func (in *CronSchedule) DeepCopy() *CronSchedule { if in == nil { return nil } out := new(CronSchedule) in.DeepCopyInto(out) return out } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/cmd/main.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package main import ( "crypto/tls" "flag" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" kbatchv1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" batchv1 "tutorial.kubebuilder.io/project/api/v1" batchv2 "tutorial.kubebuilder.io/project/api/v2" "tutorial.kubebuilder.io/project/internal/controller" webhookv1 "tutorial.kubebuilder.io/project/internal/webhook/v1" webhookv2 "tutorial.kubebuilder.io/project/internal/webhook/v2" // +kubebuilder:scaffold:imports ) // +kubebuilder:docs-gen:collapse=Imports /* */ var ( scheme = runtime.NewScheme() setupLog = ctrl.Log.WithName("setup") ) func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(kbatchv1.AddToScheme(scheme)) // we've added this ourselves utilruntime.Must(batchv1.AddToScheme(scheme)) utilruntime.Must(batchv2.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } // +kubebuilder:docs-gen:collapse=existing setup /* */ // nolint:gocyclo func main() { /* */ var metricsAddr string var metricsCertPath, metricsCertName, metricsCertKey string var webhookCertPath, webhookCertName, webhookCertKey string var enableLeaderElection bool var probeAddr string var secureMetrics bool var enableHTTP2 bool var tlsOpts []func(*tls.Config) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") flag.BoolVar(&secureMetrics, "metrics-secure", true, "If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.") flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.") flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.") flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.") flag.StringVar(&metricsCertPath, "metrics-cert-path", "", "The directory that contains the metrics server certificate.") flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.") flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.") flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") opts := zap.Options{ Development: true, } opts.BindFlags(flag.CommandLine) flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) // if the enable-http2 flag is false (the default), http/2 should be disabled // due to its vulnerabilities. More specifically, disabling http/2 will // prevent from being vulnerable to the HTTP/2 Stream Cancellation and // Rapid Reset CVEs. For more information see: // - https://github.com/advisories/GHSA-qppj-fm5r-hxr3 // - https://github.com/advisories/GHSA-4374-p667-p6c8 disableHTTP2 := func(c *tls.Config) { setupLog.Info("Disabling HTTP/2") c.NextProtos = []string{"http/1.1"} } if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) } // +kubebuilder:docs-gen:collapse=Manager Setup // Initial webhook TLS options webhookTLSOpts := tlsOpts webhookServerOptions := webhook.Options{ TLSOpts: webhookTLSOpts, } if len(webhookCertPath) > 0 { setupLog.Info("Initializing webhook certificate watcher using provided certificates", "webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey) webhookServerOptions.CertDir = webhookCertPath webhookServerOptions.CertName = webhookCertName webhookServerOptions.KeyName = webhookCertKey } webhookServer := webhook.NewServer(webhookServerOptions) // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // More info: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/server // - https://book.kubebuilder.io/reference/metrics.html metricsServerOptions := metricsserver.Options{ BindAddress: metricsAddr, SecureServing: secureMetrics, TLSOpts: tlsOpts, } if secureMetrics { // FilterProvider is used to protect the metrics endpoint with authn/authz. // These configurations ensure that only authorized users and service accounts // can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info: // https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/filters#WithAuthenticationAndAuthorization metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } // If the certificate is not specified, controller-runtime will automatically // generate self-signed certificates for the metrics server. While convenient for development and testing, // this setup is not recommended for production. // // TODO(user): If you enable certManager, uncomment the following lines: // - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates // managed by cert-manager for the metrics server. // - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification. if len(metricsCertPath) > 0 { setupLog.Info("Initializing metrics certificate watcher using provided certificates", "metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey) metricsServerOptions.CertDir = metricsCertPath metricsServerOptions.CertName = metricsCertName metricsServerOptions.KeyName = metricsCertKey } mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "80807133.tutorial.kubebuilder.io", // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily // when the Manager ends. This requires the binary to immediately end when the // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly // speeds up voluntary leader transitions as the new leader don't have to wait // LeaseDuration time first. // // In the default scaffold provided, the program ends immediately after // the manager stops, so would be fine to enable this option. However, // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, }) if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) } if err := (&controller.CronJobReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create controller", "controller", "CronJob") os.Exit(1) } /* Our existing call to SetupWebhookWithManager registers our conversion webhooks with the manager, too. */ // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err := webhookv1.SetupCronJobWebhookWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create webhook", "webhook", "CronJob") os.Exit(1) } } // nolint:goconst if os.Getenv("ENABLE_WEBHOOKS") != "false" { if err := webhookv2.SetupCronJobWebhookWithManager(mgr); err != nil { setupLog.Error(err, "Failed to create webhook", "webhook", "CronJob") os.Exit(1) } } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up health check") os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "Failed to set up ready check") os.Exit(1) } setupLog.Info("Starting manager") if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "Failed to run manager") os.Exit(1) } } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/certmanager/certificate-metrics.yaml ================================================ # The following manifests contain a self-signed issuer CR and a metrics certificate CR. # More document can be found at https://docs.cert-manager.io apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: metrics-certs # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: dnsNames: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize # replacements in the config/default/kustomization.yaml file. - SERVICE_NAME.SERVICE_NAMESPACE.svc - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer secretName: metrics-server-cert ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/certmanager/certificate-webhook.yaml ================================================ # The following manifests contain a self-signed issuer CR and a certificate CR. # More document can be found at https://docs.cert-manager.io apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize # replacements in the config/default/kustomization.yaml file. dnsNames: - SERVICE_NAME.SERVICE_NAMESPACE.svc - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer secretName: webhook-server-cert ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/certmanager/issuer.yaml ================================================ # The following manifest contains a self-signed issuer CR. # More information can be found at https://docs.cert-manager.io # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. apiVersion: cert-manager.io/v1 kind: Issuer metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: selfsigned-issuer namespace: system spec: selfSigned: {} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/certmanager/kustomization.yaml ================================================ resources: - issuer.yaml - certificate-webhook.yaml - certificate-metrics.yaml configurations: - kustomizeconfig.yaml ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/certmanager/kustomizeconfig.yaml ================================================ # This configuration is for teaching kustomize how to update name ref substitution nameReference: - kind: Issuer group: cert-manager.io fieldSpecs: - kind: Certificate group: cert-manager.io path: spec/issuerRef/name ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/crd/bases/batch.tutorial.kubebuilder.io_cronjobs.yaml ================================================ --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.20.1 name: cronjobs.batch.tutorial.kubebuilder.io spec: group: batch.tutorial.kubebuilder.io names: kind: CronJob listKind: CronJobList plural: cronjobs singular: cronjob scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: minLength: 0 type: string startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: true subresources: status: {} - name: v2 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: properties: dayOfMonth: type: string dayOfWeek: type: string hour: type: string minute: type: string month: type: string type: object startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: false subresources: status: {} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/crd/kustomization.yaml ================================================ # This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: - bases/batch.tutorial.kubebuilder.io_cronjobs.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD - path: patches/webhook_in_cronjobs.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. configurations: - kustomizeconfig.yaml ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/crd/kustomizeconfig.yaml ================================================ # This file is for teaching kustomize how to substitute name and namespace reference in CRD nameReference: - kind: Service version: v1 fieldSpecs: - kind: CustomResourceDefinition version: v1 group: apiextensions.k8s.io path: spec/conversion/webhook/clientConfig/service/name varReference: - path: metadata/annotations ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/crd/patches/webhook_in_cronjobs.yaml ================================================ # The following patch enables a conversion webhook for the CRD apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: cronjobs.batch.tutorial.kubebuilder.io spec: conversion: strategy: Webhook webhook: clientConfig: service: namespace: system name: webhook-service path: /convert conversionReviewVersions: - v1 ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/default/cert_metrics_manager_patch.yaml ================================================ # This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. # Add the volumeMount for the metrics-server certs - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true # Add the --metrics-cert-path argument for the metrics server - op: add path: /spec/template/spec/containers/0/args/- value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs # Add the metrics-server certs volume configuration - op: add path: /spec/template/spec/volumes/- value: name: metrics-certs secret: secretName: metrics-server-cert optional: false items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/default/kustomization.yaml ================================================ # Adds namespace to all resources. namespace: project-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: project- # Labels to add to all resources and selectors. #labels: #- includeSelectors: true # pairs: # someName: someValue resources: - ../crd - ../rbac - ../manager # ANCHOR: webhook-resources # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # ANCHOR_END: webhook-resources # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. - ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will # be able to communicate with the Webhook Server. #- ../network-policy # Uncomment the patches line if you enable Metrics patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: kind: Deployment # Uncomment the patches line if you enable Metrics and CertManager # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. # This patch will protect the metrics with certManager self-signed certs. - path: cert_metrics_manager_patch.yaml target: kind: Deployment # ANCHOR: webhook-patch # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - path: manager_webhook_patch.yaml target: kind: Deployment # ANCHOR_END: webhook-patch # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations replacements: - source: # Uncomment the following block to enable certificates for metrics kind: Service version: v1 name: controller-manager-metrics-service fieldPath: metadata.name targets: - select: kind: Certificate group: cert-manager.io version: v1 name: metrics-certs fieldPaths: - spec.dnsNames.0 - spec.dnsNames.1 options: delimiter: '.' index: 0 create: true - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor kind: ServiceMonitor group: monitoring.coreos.com version: v1 name: controller-manager-metrics-monitor fieldPaths: - spec.endpoints.0.tlsConfig.serverName options: delimiter: '.' index: 0 create: true - source: kind: Service version: v1 name: controller-manager-metrics-service fieldPath: metadata.namespace targets: - select: kind: Certificate group: cert-manager.io version: v1 name: metrics-certs fieldPaths: - spec.dnsNames.0 - spec.dnsNames.1 options: delimiter: '.' index: 1 create: true - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor kind: ServiceMonitor group: monitoring.coreos.com version: v1 name: controller-manager-metrics-monitor fieldPaths: - spec.endpoints.0.tlsConfig.serverName options: delimiter: '.' index: 1 create: true # ANCHOR: webhook-replacements - source: # Uncomment the following block if you have any webhook kind: Service version: v1 name: webhook-service fieldPath: .metadata.name # Name of the service targets: - select: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPaths: - .spec.dnsNames.0 - .spec.dnsNames.1 options: delimiter: '.' index: 0 create: true - source: kind: Service version: v1 name: webhook-service fieldPath: .metadata.namespace # Namespace of the service targets: - select: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPaths: - .spec.dnsNames.0 - .spec.dnsNames.1 options: delimiter: '.' index: 1 create: true - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) kind: Certificate group: cert-manager.io version: v1 name: serving-cert # This name should match the one in certificate.yaml fieldPath: .metadata.namespace # Namespace of the certificate CR targets: - select: kind: ValidatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 0 create: true - source: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.name targets: - select: kind: ValidatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 1 create: true - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.namespace # Namespace of the certificate CR targets: - select: kind: MutatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 0 create: true - source: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.name targets: - select: kind: MutatingWebhookConfiguration fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 1 create: true # ANCHOR_END: webhook-replacements - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.namespace # Namespace of the certificate CR targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. - select: kind: CustomResourceDefinition name: cronjobs.batch.tutorial.kubebuilder.io fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 0 create: true # +kubebuilder:scaffold:crdkustomizecainjectionns - source: kind: Certificate group: cert-manager.io version: v1 name: serving-cert fieldPath: .metadata.name targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. - select: kind: CustomResourceDefinition name: cronjobs.batch.tutorial.kubebuilder.io fieldPaths: - .metadata.annotations.[cert-manager.io/inject-ca-from] options: delimiter: '/' index: 1 create: true # +kubebuilder:scaffold:crdkustomizecainjectionname ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/default/manager_metrics_patch.yaml ================================================ # This patch adds the args to allow exposing the metrics endpoint using HTTPS - op: add path: /spec/template/spec/containers/0/args/0 value: --metrics-bind-address=:8443 ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/default/manager_webhook_patch.yaml ================================================ # This patch ensures the webhook certificates are properly mounted in the manager container. # It configures the necessary arguments, volumes, volume mounts, and container ports. # Add the --webhook-cert-path argument for configuring the webhook certificate path - op: add path: /spec/template/spec/containers/0/args/- value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs # Add the volumeMount for the webhook certificates - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs readOnly: true # Add the port configuration for the webhook server - op: add path: /spec/template/spec/containers/0/ports/- value: containerPort: 9443 name: webhook-server protocol: TCP # Add the volume configuration for the webhook certificates - op: add path: /spec/template/spec/volumes/- value: name: webhook-certs secret: secretName: webhook-server-cert ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/default/metrics_service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-service namespace: system spec: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 selector: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/manager/kustomization.yaml ================================================ resources: - manager.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller newName: controller newTag: latest ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/manager/manager.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: system --- apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize spec: selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: control-plane: controller-manager app.kubernetes.io/name: project spec: # TODO(user): Uncomment the following code to configure the nodeAffinity expression # according to the platforms which are supported by your solution. # It is considered best practice to support multiple architectures. You can # build your manager image using the makefile target docker-buildx. # affinity: # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: kubernetes.io/arch # operator: In # values: # - amd64 # - arm64 # - ppc64le # - s390x # - key: kubernetes.io/os # operator: In # values: # - linux securityContext: # Projects are configured by default to adhere to the "restricted" Pod Security Standards. # This ensures that deployments meet the highest security requirements for Kubernetes. # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - command: - /manager args: - --leader-elect - --health-probe-bind-address=:8081 image: controller:latest name: manager ports: [] securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: - "ALL" livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 # TODO(user): Configure the resources accordingly based on the project requirements. # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi volumeMounts: [] volumes: [] serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/network-policy/allow-metrics-traffic.yaml ================================================ # This NetworkPolicy allows ingress traffic # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those # namespaces are able to gather data from the metrics endpoint. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: allow-metrics-traffic namespace: system spec: podSelector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project policyTypes: - Ingress ingress: # This allows ingress traffic from any namespace with the label metrics: enabled - from: - namespaceSelector: matchLabels: metrics: enabled # Only from namespaces with this label ports: - port: 8443 protocol: TCP ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/network-policy/allow-webhook-traffic.yaml ================================================ # This NetworkPolicy allows ingress traffic to your webhook server running # as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks # will only work when applied in namespaces labeled with 'webhook: enabled' apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: allow-webhook-traffic namespace: system spec: podSelector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project policyTypes: - Ingress ingress: # This allows ingress traffic from any namespace with the label webhook: enabled - from: - namespaceSelector: matchLabels: webhook: enabled # Only from namespaces with this label ports: - port: 443 protocol: TCP ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/network-policy/kustomization.yaml ================================================ resources: - allow-webhook-traffic.yaml - allow-metrics-traffic.yaml ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/prometheus/kustomization.yaml ================================================ resources: - monitor.yaml # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus # to securely reference certificates created and managed by cert-manager. # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml # to mount the "metrics-server-cert" secret in the Manager Deployment. patches: - path: monitor_tls_patch.yaml target: kind: ServiceMonitor ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/prometheus/monitor.yaml ================================================ # Prometheus Monitor Service (Metrics) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: control-plane: controller-manager app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system spec: endpoints: - path: /metrics port: https # Ensure this is the name of the port that exposes HTTPS metrics scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables # certificate verification, exposing the system to potential man-in-the-middle attacks. # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, # which securely references the certificate from the 'metrics-server-cert' secret. insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/prometheus/monitor_tls_patch.yaml ================================================ # Patch for Prometheus ServiceMonitor to enable secure TLS configuration # using certificates managed by cert-manager - op: replace path: /spec/endpoints/0/tlsConfig value: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc insecureSkipVerify: false ca: secret: name: metrics-server-cert key: ca.crt cert: secret: name: metrics-server-cert key: tls.crt keySecret: name: metrics-server-cert key: tls.key ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/cronjob_admin_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants full permissions ('*') over batch.tutorial.kubebuilder.io. # This role is intended for users authorized to modify roles and bindings within the cluster, # enabling them to delegate specific permissions to other users or groups as needed. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-admin-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - '*' - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/cronjob_editor_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants permissions to create, update, and delete resources within the batch.tutorial.kubebuilder.io. # This role is intended for users who need to manage these resources # but should not control RBAC or manage permissions for others. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-editor-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/cronjob_viewer_role.yaml ================================================ # This rule is not used by the project project itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants read-only access to batch.tutorial.kubebuilder.io resources. # This role is intended for users who need visibility into these resources # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-viewer-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - get - list - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/kustomization.yaml ================================================ resources: # All RBAC will be applied under this service account in # the deployment namespace. You may comment out this resource # if your manager will use a service account that exists at # runtime. Be sure to update RoleBinding and ClusterRoleBinding # subjects if changing service account names. - service_account.yaml - role.yaml - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # The following RBAC configurations are used to protect # the metrics endpoint with authn/authz. These configurations # ensure that only authorized users and service accounts # can access the metrics endpoint. Comment the following # permissions if you want to disable this protection. # More info: https://book.kubebuilder.io/reference/metrics.html - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml # For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by # default, aiding admins in cluster management. Those roles are # not used by the project itself. You can comment the following lines # if you do not want those helpers be installed with your Project. - cronjob_admin_role.yaml - cronjob_editor_role.yaml - cronjob_viewer_role.yaml ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/leader_election_role.yaml ================================================ # permissions to do leader election. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/leader_election_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: leader-election-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/metrics_auth_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-auth-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/metrics_auth_role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: metrics-auth-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: metrics-auth-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/metrics_reader_role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-reader rules: - nonResourceURLs: - "/metrics" verbs: - get ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/role.yaml ================================================ --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: manager-role rules: - apiGroups: - batch resources: - jobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch resources: - jobs/status verbs: - get - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/finalizers verbs: - update - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get - patch - update ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/role_binding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: manager-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/rbac/service_account.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: controller-manager namespace: system ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/samples/batch_v1_cronjob.yaml ================================================ apiVersion: batch.tutorial.kubebuilder.io/v1 kind: CronJob metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-sample spec: schedule: "*/1 * * * *" startingDeadlineSeconds: 60 concurrencyPolicy: Allow # explicitly specify, but Allow is also default. jobTemplate: spec: template: spec: securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false restartPolicy: OnFailure ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/samples/batch_v2_cronjob.yaml ================================================ apiVersion: batch.tutorial.kubebuilder.io/v2 kind: CronJob metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: cronjob-sample spec: schedule: minute: "*/1" startingDeadlineSeconds: 60 concurrencyPolicy: Allow # explicitly specify, but Allow is also default. jobTemplate: spec: template: spec: securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false restartPolicy: OnFailure ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/samples/kustomization.yaml ================================================ ## Append samples of your project ## resources: - batch_v1_cronjob.yaml - batch_v2_cronjob.yaml # +kubebuilder:scaffold:manifestskustomizesamples ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/webhook/kustomization.yaml ================================================ resources: - manifests.yaml - service.yaml ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/webhook/manifests.yaml ================================================ --- apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: mutating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: system path: /mutate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: mcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None - admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: system path: /mutate-batch-tutorial-kubebuilder-io-v2-cronjob failurePolicy: Fail name: mcronjob-v2.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v2 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: validating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: system path: /validate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: vcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None - admissionReviewVersions: - v1 clientConfig: service: name: webhook-service namespace: system path: /validate-batch-tutorial-kubebuilder-io-v2-cronjob failurePolicy: Fail name: vcronjob-v2.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v2 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/config/webhook/service.yaml ================================================ apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/name: project app.kubernetes.io/managed-by: kustomize name: webhook-service namespace: system spec: ports: - port: 443 protocol: TCP targetPort: 9443 selector: control-plane: controller-manager app.kubernetes.io/name: project ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/.helmignore ================================================ # Patterns to ignore when building Helm packages. # Operating system files .DS_Store # Version control directories .git/ .gitignore .bzr/ .hg/ .hgignore .svn/ # Backup and temporary files *.swp *.tmp *.bak *.orig *~ # IDE and editor-related files .idea/ .vscode/ # Helm chart artifacts dist/chart/*.tgz ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/Chart.yaml ================================================ apiVersion: v2 name: project description: A Helm chart to distribute project type: application version: 0.1.0 appVersion: "0.1.0" keywords: - kubernetes - operator annotations: kubebuilder.io/generated-by: kubebuilder ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/NOTES.txt ================================================ Thank you for installing {{ .Chart.Name }}. Your release is named {{ .Release.Name }}. The controller and CRDs have been installed in namespace {{ .Release.Namespace }}. To verify the installation: kubectl get pods -n {{ .Release.Namespace }} kubectl get customresourcedefinitions To learn more about the release, try: $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }} $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "project.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "project.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Namespace for generated references. Always uses the Helm release namespace. */}} {{- define "project.namespaceName" -}} {{- .Release.Namespace }} {{- end }} {{/* Resource name with proper truncation for Kubernetes 63-character limit. Takes a dict with: - .suffix: Resource name suffix (e.g., "metrics", "webhook") - .context: Template context (root context with .Values, .Release, etc.) Dynamically calculates safe truncation to ensure total name length <= 63 chars. */}} {{- define "project.resourceName" -}} {{- $fullname := include "project.fullname" .context }} {{- $suffix := .suffix }} {{- $maxLen := sub 62 (len $suffix) | int }} {{- if gt (len $fullname) $maxLen }} {{- printf "%s-%s" (trunc $maxLen $fullname | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" $fullname $suffix | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/cert-manager/metrics-certs.yaml ================================================ {{- if and .Values.certManager.enable .Values.metrics.enable }} apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "metrics-certs" "context" $) }} namespace: {{ .Release.Namespace }} spec: dnsNames: - {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc - {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local issuerRef: kind: Issuer name: {{ include "project.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} secretName: metrics-server-cert {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/cert-manager/selfsigned-issuer.yaml ================================================ {{- if .Values.certManager.enable }} apiVersion: cert-manager.io/v1 kind: Issuer metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} namespace: {{ .Release.Namespace }} spec: selfSigned: {} {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/cert-manager/serving-cert.yaml ================================================ {{- if .Values.certManager.enable }} apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "serving-cert" "context" $) }} namespace: {{ .Release.Namespace }} spec: dnsNames: - {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc - {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local issuerRef: kind: Issuer name: {{ include "project.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }} secretName: webhook-server-cert {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/crd/cronjobs.batch.tutorial.kubebuilder.io.yaml ================================================ {{- if .Values.crd.enable }} apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: {{- if .Values.crd.keep }} "helm.sh/resource-policy": keep {{- end }} cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "project.resourceName" (dict "suffix" "serving-cert" "context" $) }} controller-gen.kubebuilder.io/version: v0.20.1 name: cronjobs.batch.tutorial.kubebuilder.io spec: conversion: strategy: Webhook webhook: clientConfig: service: name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} path: /convert conversionReviewVersions: - v1 group: batch.tutorial.kubebuilder.io names: kind: CronJob listKind: CronJobList plural: cronjobs singular: cronjob scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: minLength: 0 type: string startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: true subresources: status: {} - name: v2 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: properties: dayOfMonth: type: string dayOfWeek: type: string hour: type: string minute: type: string month: type: string type: object startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: false subresources: status: {} {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/manager/manager.yaml ================================================ apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} spec: replicas: {{ .Values.manager.replicas }} selector: matchLabels: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} app.kubernetes.io/managed-by: {{ .Release.Service }} control-plane: controller-manager spec: {{- with .Values.manager.tolerations }} tolerations: {{ toYaml . | nindent 10 }} {{- end }} {{- with .Values.manager.affinity }} affinity: {{ toYaml . | nindent 10 }} {{- end }} {{- with .Values.manager.nodeSelector }} nodeSelector: {{ toYaml . | nindent 10 }} {{- end }} containers: - args: {{- if .Values.metrics.enable }} - --metrics-bind-address=:{{ .Values.metrics.port }} {{- else }} # Bind to :0 to disable the controller-runtime managed metrics server - --metrics-bind-address=0 {{- end }} - --health-probe-bind-address=:8081 {{- range .Values.manager.args }} - {{ . }} {{- end }} {{- if and .Values.certManager.enable .Values.metrics.enable }} - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs {{- end }} {{- if .Values.certManager.enable }} - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs {{- end }} command: - /manager image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}" imagePullPolicy: {{ .Values.manager.image.pullPolicy }} livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 name: manager ports: - containerPort: {{ .Values.webhook.port }} name: webhook-server protocol: TCP readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 resources: {{- if .Values.manager.resources }} {{- toYaml .Values.manager.resources | nindent 10 }} {{- else }} {} {{- end }} securityContext: {{- if .Values.manager.securityContext }} {{- toYaml .Values.manager.securityContext | nindent 10 }} {{- else }} {} {{- end }} volumeMounts: {{- if and .Values.certManager.enable .Values.metrics.enable }} - mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true {{- end }} {{- if .Values.certManager.enable }} - mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs readOnly: true {{- end }} securityContext: {{- if .Values.manager.podSecurityContext }} {{- toYaml .Values.manager.podSecurityContext | nindent 8 }} {{- else }} {} {{- end }} serviceAccountName: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} terminationGracePeriodSeconds: 10 volumes: {{- if and .Values.certManager.enable .Values.metrics.enable }} - name: metrics-certs secret: items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key optional: false secretName: metrics-server-cert {{- end }} {{- if .Values.certManager.enable }} - name: webhook-certs secret: secretName: webhook-server-cert {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/metrics/controller-manager-metrics-service.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }} namespace: {{ .Release.Namespace }} spec: ports: - name: https port: {{ .Values.metrics.port }} protocol: TCP targetPort: {{ .Values.metrics.port }} selector: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/prometheus/controller-manager-metrics-monitor.yaml ================================================ {{- if .Values.prometheus.enable }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} control-plane: controller-manager name: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-monitor" "context" $) }} namespace: {{ .Release.Namespace }} spec: endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token path: /metrics port: https scheme: https tlsConfig: ca: secret: key: ca.crt name: metrics-server-cert cert: secret: key: tls.crt name: metrics-server-cert insecureSkipVerify: false keySecret: key: tls.key name: metrics-server-cert serverName: {{ include "project.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc selector: matchLabels: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/controller-manager.yaml ================================================ apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/cronjob-admin-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "cronjob-admin-role" "context" $) }} rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - '*' - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/cronjob-editor-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "cronjob-editor-role" "context" $) }} rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/cronjob-viewer-role.yaml ================================================ {{- if .Values.rbacHelpers.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "cronjob-viewer-role" "context" $) }} rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - get - list - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/leader-election-role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "leader-election-role" "context" $) }} namespace: {{ .Release.Namespace }} rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/leader-election-rolebinding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "leader-election-rolebinding" "context" $) }} namespace: {{ .Release.Namespace }} roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: {{ include "project.resourceName" (dict "suffix" "leader-election-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/manager-role.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "manager-role" "context" $) }} rules: - apiGroups: - batch resources: - jobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch resources: - jobs/status verbs: - get - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/finalizers verbs: - update - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get - patch - update ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/manager-rolebinding.yaml ================================================ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "manager-rolebinding" "context" $) }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ include "project.resourceName" (dict "suffix" "manager-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/metrics-auth-role.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/metrics-auth-rolebinding.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-rolebinding" "context" $) }} roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: {{ include "project.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }} subjects: - kind: ServiceAccount name: {{ include "project.resourceName" (dict "suffix" "controller-manager" "context" $) }} namespace: {{ .Release.Namespace }} {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/rbac/metrics-reader.yaml ================================================ {{- if .Values.metrics.enable }} apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "project.resourceName" (dict "suffix" "metrics-reader" "context" $) }} rules: - nonResourceURLs: - /metrics verbs: - get {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/webhook/mutating-webhook-configuration.yaml ================================================ {{- if .Values.webhook.enable }} apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: annotations: {{- if .Values.certManager.enable }} cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "project.resourceName" (dict "suffix" "serving-cert" "context" $) }} {{- end }} name: {{ include "project.resourceName" (dict "suffix" "mutating-webhook-configuration" "context" $) }} webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} path: /mutate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: mcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None - admissionReviewVersions: - v1 clientConfig: service: name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} path: /mutate-batch-tutorial-kubebuilder-io-v2-cronjob failurePolicy: Fail name: mcronjob-v2.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v2 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/webhook/validating-webhook-configuration.yaml ================================================ {{- if .Values.webhook.enable }} apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: annotations: {{- if .Values.certManager.enable }} cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "project.resourceName" (dict "suffix" "serving-cert" "context" $) }} {{- end }} name: {{ include "project.resourceName" (dict "suffix" "validating-webhook-configuration" "context" $) }} webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} path: /validate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: vcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None - admissionReviewVersions: - v1 clientConfig: service: name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} path: /validate-batch-tutorial-kubebuilder-io-v2-cronjob failurePolicy: Fail name: vcronjob-v2.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v2 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/templates/webhook/webhook-service.yaml ================================================ {{- if .Values.webhook.enable }} apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: {{ .Release.Service }} app.kubernetes.io/name: {{ include "project.name" . }} helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} app.kubernetes.io/instance: {{ .Release.Name }} name: {{ include "project.resourceName" (dict "suffix" "webhook-service" "context" $) }} namespace: {{ .Release.Namespace }} spec: ports: - port: 443 protocol: TCP targetPort: {{ .Values.webhook.port }} selector: app.kubernetes.io/name: {{ include "project.name" . }} control-plane: controller-manager {{- end }} ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/chart/values.yaml ================================================ ## String to partially override chart.fullname template (will maintain the release name) ## # nameOverride: "" ## String to fully override chart.fullname template ## # fullnameOverride: "" ## Configure the controller manager deployment ## manager: replicas: 1 image: repository: controller tag: latest pullPolicy: IfNotPresent ## Arguments ## args: - --leader-elect ## Environment variables ## env: [] ## Env overrides (--set manager.envOverrides.VAR=value) ## Same name in env above: this value takes precedence. ## envOverrides: {} ## Image pull secrets ## imagePullSecrets: [] ## Pod-level security settings ## podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault ## Container-level security settings ## securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true ## Resource limits and requests ## resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi ## Manager pod's affinity ## affinity: {} ## Manager pod's node selector ## nodeSelector: {} ## Manager pod's tolerations ## tolerations: [] ## Helper RBAC roles for managing custom resources ## rbacHelpers: # Install convenience admin/editor/viewer roles for CRDs enable: false ## Custom Resource Definitions ## crd: # Install CRDs with the chart enable: true # Keep CRDs when uninstalling keep: true ## Controller metrics endpoint. ## Enable to expose /metrics endpoint with RBAC protection. ## metrics: enable: true # Metrics server port port: 8443 ## Cert-manager integration for TLS certificates. ## Required for webhook certificates and metrics endpoint certificates. ## certManager: enable: true ## Webhook server configuration ## webhook: enable: true # Webhook server port port: 9443 ## Prometheus ServiceMonitor for metrics scraping. ## Requires prometheus-operator to be installed in the cluster. ## prometheus: enable: false ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/dist/install.yaml ================================================ apiVersion: v1 kind: Namespace metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-system --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: project-system/project-serving-cert controller-gen.kubebuilder.io/version: v0.20.1 name: cronjobs.batch.tutorial.kubebuilder.io spec: conversion: strategy: Webhook webhook: clientConfig: service: name: project-webhook-service namespace: project-system path: /convert conversionReviewVersions: - v1 group: batch.tutorial.kubebuilder.io names: kind: CronJob listKind: CronJobList plural: cronjobs singular: cronjob scope: Namespaced versions: - name: v1 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: minLength: 0 type: string startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: true subresources: status: {} - name: v2 schema: openAPIV3Schema: properties: apiVersion: type: string kind: type: string metadata: type: object spec: properties: concurrencyPolicy: default: Allow enum: - Allow - Forbid - Replace type: string failedJobsHistoryLimit: format: int32 minimum: 0 type: integer jobTemplate: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer backoffLimit: format: int32 type: integer backoffLimitPerIndex: format: int32 type: integer completionMode: type: string completions: format: int32 type: integer managedBy: type: string manualSelector: type: boolean maxFailedIndexes: format: int32 type: integer parallelism: format: int32 type: integer podFailurePolicy: properties: rules: items: properties: action: type: string onExitCodes: properties: containerName: type: string operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator - values type: object onPodConditions: items: properties: status: type: string type: type: string required: - type type: object type: array x-kubernetes-list-type: atomic required: - action type: object type: array x-kubernetes-list-type: atomic required: - rules type: object podReplacementPolicy: type: string selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic successPolicy: properties: rules: items: properties: succeededCount: format: int32 type: integer succeededIndexes: type: string type: object type: array x-kubernetes-list-type: atomic required: - rules type: object suspend: type: boolean template: properties: metadata: type: object spec: properties: activeDeadlineSeconds: format: int64 type: integer affinity: properties: nodeAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: preference: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic weight: format: int32 type: integer required: - preference - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: properties: nodeSelectorTerms: items: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchFields: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-type: atomic required: - nodeSelectorTerms type: object x-kubernetes-map-type: atomic type: object podAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object podAntiAffinity: properties: preferredDuringSchedulingIgnoredDuringExecution: items: properties: podAffinityTerm: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object weight: format: int32 type: integer required: - podAffinityTerm - weight type: object type: array x-kubernetes-list-type: atomic requiredDuringSchedulingIgnoredDuringExecution: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic mismatchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic namespaceSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic namespaces: items: type: string type: array x-kubernetes-list-type: atomic topologyKey: type: string required: - topologyKey type: object type: array x-kubernetes-list-type: atomic type: object type: object automountServiceAccountToken: type: boolean containers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map dnsConfig: properties: nameservers: items: type: string type: array x-kubernetes-list-type: atomic options: items: properties: name: type: string value: type: string type: object type: array x-kubernetes-list-type: atomic searches: items: type: string type: array x-kubernetes-list-type: atomic type: object dnsPolicy: type: string enableServiceLinks: type: boolean ephemeralContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean targetContainerName: type: string terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map hostAliases: items: properties: hostnames: items: type: string type: array x-kubernetes-list-type: atomic ip: type: string required: - ip type: object type: array x-kubernetes-list-map-keys: - ip x-kubernetes-list-type: map hostIPC: type: boolean hostNetwork: type: boolean hostPID: type: boolean hostUsers: type: boolean hostname: type: string hostnameOverride: type: string imagePullSecrets: items: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map initContainers: items: properties: args: items: type: string type: array x-kubernetes-list-type: atomic command: items: type: string type: array x-kubernetes-list-type: atomic env: items: properties: name: type: string value: type: string valueFrom: properties: configMapKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic fileKeyRef: properties: key: type: string optional: default: false type: boolean path: type: string volumeName: type: string required: - key - path - volumeName type: object x-kubernetes-map-type: atomic resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic secretKeyRef: properties: key: type: string name: default: "" type: string optional: type: boolean required: - key type: object x-kubernetes-map-type: atomic type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map envFrom: items: properties: configMapRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic prefix: type: string secretRef: properties: name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic type: object type: array x-kubernetes-list-type: atomic image: type: string imagePullPolicy: type: string lifecycle: properties: postStart: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object preStop: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object sleep: properties: seconds: format: int64 type: integer required: - seconds type: object tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object type: object stopSignal: type: string type: object livenessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object name: type: string ports: items: properties: containerPort: format: int32 type: integer hostIP: type: string hostPort: format: int32 type: integer name: type: string protocol: default: TCP type: string required: - containerPort type: object type: array x-kubernetes-list-map-keys: - containerPort - protocol x-kubernetes-list-type: map readinessProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object resizePolicy: items: properties: resourceName: type: string restartPolicy: type: string required: - resourceName - restartPolicy type: object type: array x-kubernetes-list-type: atomic resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string restartPolicyRules: items: properties: action: type: string exitCodes: properties: operator: type: string values: items: format: int32 type: integer type: array x-kubernetes-list-type: set required: - operator type: object required: - action type: object type: array x-kubernetes-list-type: atomic securityContext: properties: allowPrivilegeEscalation: type: boolean appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object capabilities: properties: add: items: type: string type: array x-kubernetes-list-type: atomic drop: items: type: string type: array x-kubernetes-list-type: atomic type: object privileged: type: boolean procMount: type: string readOnlyRootFilesystem: type: boolean runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object startupProbe: properties: exec: properties: command: items: type: string type: array x-kubernetes-list-type: atomic type: object failureThreshold: format: int32 type: integer grpc: properties: port: format: int32 type: integer service: default: "" type: string required: - port type: object httpGet: properties: host: type: string httpHeaders: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic path: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true scheme: type: string required: - port type: object initialDelaySeconds: format: int32 type: integer periodSeconds: format: int32 type: integer successThreshold: format: int32 type: integer tcpSocket: properties: host: type: string port: anyOf: - type: integer - type: string x-kubernetes-int-or-string: true required: - port type: object terminationGracePeriodSeconds: format: int64 type: integer timeoutSeconds: format: int32 type: integer type: object stdin: type: boolean stdinOnce: type: boolean terminationMessagePath: type: string terminationMessagePolicy: type: string tty: type: boolean volumeDevices: items: properties: devicePath: type: string name: type: string required: - devicePath - name type: object type: array x-kubernetes-list-map-keys: - devicePath x-kubernetes-list-type: map volumeMounts: items: properties: mountPath: type: string mountPropagation: type: string name: type: string readOnly: type: boolean recursiveReadOnly: type: string subPath: type: string subPathExpr: type: string required: - mountPath - name type: object type: array x-kubernetes-list-map-keys: - mountPath x-kubernetes-list-type: map workingDir: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map nodeName: type: string nodeSelector: additionalProperties: type: string type: object x-kubernetes-map-type: atomic os: properties: name: type: string required: - name type: object overhead: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object preemptionPolicy: type: string priority: format: int32 type: integer priorityClassName: type: string readinessGates: items: properties: conditionType: type: string required: - conditionType type: object type: array x-kubernetes-list-type: atomic resourceClaims: items: properties: name: type: string resourceClaimName: type: string resourceClaimTemplateName: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map resources: properties: claims: items: properties: name: type: string request: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object restartPolicy: type: string runtimeClassName: type: string schedulerName: type: string schedulingGates: items: properties: name: type: string required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map securityContext: properties: appArmorProfile: properties: localhostProfile: type: string type: type: string required: - type type: object fsGroup: format: int64 type: integer fsGroupChangePolicy: type: string runAsGroup: format: int64 type: integer runAsNonRoot: type: boolean runAsUser: format: int64 type: integer seLinuxChangePolicy: type: string seLinuxOptions: properties: level: type: string role: type: string type: type: string user: type: string type: object seccompProfile: properties: localhostProfile: type: string type: type: string required: - type type: object supplementalGroups: items: format: int64 type: integer type: array x-kubernetes-list-type: atomic supplementalGroupsPolicy: type: string sysctls: items: properties: name: type: string value: type: string required: - name - value type: object type: array x-kubernetes-list-type: atomic windowsOptions: properties: gmsaCredentialSpec: type: string gmsaCredentialSpecName: type: string hostProcess: type: boolean runAsUserName: type: string type: object type: object serviceAccount: type: string serviceAccountName: type: string setHostnameAsFQDN: type: boolean shareProcessNamespace: type: boolean subdomain: type: string terminationGracePeriodSeconds: format: int64 type: integer tolerations: items: properties: effect: type: string key: type: string operator: type: string tolerationSeconds: format: int64 type: integer value: type: string type: object type: array x-kubernetes-list-type: atomic topologySpreadConstraints: items: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic matchLabelKeys: items: type: string type: array x-kubernetes-list-type: atomic maxSkew: format: int32 type: integer minDomains: format: int32 type: integer nodeAffinityPolicy: type: string nodeTaintsPolicy: type: string topologyKey: type: string whenUnsatisfiable: type: string required: - maxSkew - topologyKey - whenUnsatisfiable type: object type: array x-kubernetes-list-map-keys: - topologyKey - whenUnsatisfiable x-kubernetes-list-type: map volumes: items: properties: awsElasticBlockStore: properties: fsType: type: string partition: format: int32 type: integer readOnly: type: boolean volumeID: type: string required: - volumeID type: object azureDisk: properties: cachingMode: type: string diskName: type: string diskURI: type: string fsType: default: ext4 type: string kind: type: string readOnly: default: false type: boolean required: - diskName - diskURI type: object azureFile: properties: readOnly: type: boolean secretName: type: string shareName: type: string required: - secretName - shareName type: object cephfs: properties: monitors: items: type: string type: array x-kubernetes-list-type: atomic path: type: string readOnly: type: boolean secretFile: type: string secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: type: string required: - monitors type: object cinder: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeID: type: string required: - volumeID type: object configMap: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic csi: properties: driver: type: string fsType: type: string nodePublishSecretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic readOnly: type: boolean volumeAttributes: additionalProperties: type: string type: object required: - driver type: object downwardAPI: properties: defaultMode: format: int32 type: integer items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object emptyDir: properties: medium: type: string sizeLimit: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object ephemeral: properties: volumeClaimTemplate: properties: metadata: type: object spec: properties: accessModes: items: type: string type: array x-kubernetes-list-type: atomic dataSource: properties: apiGroup: type: string kind: type: string name: type: string required: - kind - name type: object x-kubernetes-map-type: atomic dataSourceRef: properties: apiGroup: type: string kind: type: string name: type: string namespace: type: string required: - kind - name type: object resources: properties: limits: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object requests: additionalProperties: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object type: object selector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic storageClassName: type: string volumeAttributesClassName: type: string volumeMode: type: string volumeName: type: string type: object required: - spec type: object type: object fc: properties: fsType: type: string lun: format: int32 type: integer readOnly: type: boolean targetWWNs: items: type: string type: array x-kubernetes-list-type: atomic wwids: items: type: string type: array x-kubernetes-list-type: atomic type: object flexVolume: properties: driver: type: string fsType: type: string options: additionalProperties: type: string type: object readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic required: - driver type: object flocker: properties: datasetName: type: string datasetUUID: type: string type: object gcePersistentDisk: properties: fsType: type: string partition: format: int32 type: integer pdName: type: string readOnly: type: boolean required: - pdName type: object gitRepo: properties: directory: type: string repository: type: string revision: type: string required: - repository type: object glusterfs: properties: endpoints: type: string path: type: string readOnly: type: boolean required: - endpoints - path type: object hostPath: properties: path: type: string type: type: string required: - path type: object image: properties: pullPolicy: type: string reference: type: string type: object iscsi: properties: chapAuthDiscovery: type: boolean chapAuthSession: type: boolean fsType: type: string initiatorName: type: string iqn: type: string iscsiInterface: default: default type: string lun: format: int32 type: integer portals: items: type: string type: array x-kubernetes-list-type: atomic readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic targetPortal: type: string required: - iqn - lun - targetPortal type: object name: type: string nfs: properties: path: type: string readOnly: type: boolean server: type: string required: - path - server type: object persistentVolumeClaim: properties: claimName: type: string readOnly: type: boolean required: - claimName type: object photonPersistentDisk: properties: fsType: type: string pdID: type: string required: - pdID type: object portworxVolume: properties: fsType: type: string readOnly: type: boolean volumeID: type: string required: - volumeID type: object projected: properties: defaultMode: format: int32 type: integer sources: items: properties: clusterTrustBundle: properties: labelSelector: properties: matchExpressions: items: properties: key: type: string operator: type: string values: items: type: string type: array x-kubernetes-list-type: atomic required: - key - operator type: object type: array x-kubernetes-list-type: atomic matchLabels: additionalProperties: type: string type: object type: object x-kubernetes-map-type: atomic name: type: string optional: type: boolean path: type: string signerName: type: string required: - path type: object configMap: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic downwardAPI: properties: items: items: properties: fieldRef: properties: apiVersion: type: string fieldPath: type: string required: - fieldPath type: object x-kubernetes-map-type: atomic mode: format: int32 type: integer path: type: string resourceFieldRef: properties: containerName: type: string divisor: anyOf: - type: integer - type: string pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true resource: type: string required: - resource type: object x-kubernetes-map-type: atomic required: - path type: object type: array x-kubernetes-list-type: atomic type: object podCertificate: properties: certificateChainPath: type: string credentialBundlePath: type: string keyPath: type: string keyType: type: string maxExpirationSeconds: format: int32 type: integer signerName: type: string userAnnotations: additionalProperties: type: string type: object required: - keyType - signerName type: object secret: properties: items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic name: default: "" type: string optional: type: boolean type: object x-kubernetes-map-type: atomic serviceAccountToken: properties: audience: type: string expirationSeconds: format: int64 type: integer path: type: string required: - path type: object type: object type: array x-kubernetes-list-type: atomic type: object quobyte: properties: group: type: string readOnly: type: boolean registry: type: string tenant: type: string user: type: string volume: type: string required: - registry - volume type: object rbd: properties: fsType: type: string image: type: string keyring: default: /etc/ceph/keyring type: string monitors: items: type: string type: array x-kubernetes-list-type: atomic pool: default: rbd type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic user: default: admin type: string required: - image - monitors type: object scaleIO: properties: fsType: default: xfs type: string gateway: type: string protectionDomain: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic sslEnabled: type: boolean storageMode: default: ThinProvisioned type: string storagePool: type: string system: type: string volumeName: type: string required: - gateway - secretRef - system type: object secret: properties: defaultMode: format: int32 type: integer items: items: properties: key: type: string mode: format: int32 type: integer path: type: string required: - key - path type: object type: array x-kubernetes-list-type: atomic optional: type: boolean secretName: type: string type: object storageos: properties: fsType: type: string readOnly: type: boolean secretRef: properties: name: default: "" type: string type: object x-kubernetes-map-type: atomic volumeName: type: string volumeNamespace: type: string type: object vsphereVolume: properties: fsType: type: string storagePolicyID: type: string storagePolicyName: type: string volumePath: type: string required: - volumePath type: object required: - name type: object type: array x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map workloadRef: properties: name: type: string podGroup: type: string podGroupReplicaKey: type: string required: - name - podGroup type: object required: - containers type: object type: object ttlSecondsAfterFinished: format: int32 type: integer required: - template type: object type: object schedule: properties: dayOfMonth: type: string dayOfWeek: type: string hour: type: string minute: type: string month: type: string type: object startingDeadlineSeconds: format: int64 minimum: 0 type: integer successfulJobsHistoryLimit: format: int32 minimum: 0 type: integer suspend: type: boolean required: - jobTemplate - schedule type: object status: properties: active: items: properties: apiVersion: type: string fieldPath: type: string kind: type: string name: type: string namespace: type: string resourceVersion: type: string uid: type: string type: object x-kubernetes-map-type: atomic maxItems: 10 minItems: 1 type: array x-kubernetes-list-type: atomic conditions: items: properties: lastTransitionTime: format: date-time type: string message: maxLength: 32768 type: string observedGeneration: format: int64 minimum: 0 type: integer reason: maxLength: 1024 minLength: 1 pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string status: enum: - "True" - "False" - Unknown type: string type: maxLength: 316 pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - lastTransitionTime - message - reason - status - type type: object type: array x-kubernetes-list-map-keys: - type x-kubernetes-list-type: map lastScheduleTime: format: date-time type: string type: object required: - spec type: object served: true storage: false subresources: status: {} --- apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-leader-election-role namespace: project-system rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-cronjob-admin-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - '*' - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-cronjob-editor-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-cronjob-viewer-role rules: - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - get - list - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-manager-role rules: - apiGroups: - batch resources: - jobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch resources: - jobs/status verbs: - get - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs verbs: - create - delete - get - list - patch - update - watch - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/finalizers verbs: - update - apiGroups: - batch.tutorial.kubebuilder.io resources: - cronjobs/status verbs: - get - patch - update --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-metrics-auth-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: project-metrics-reader rules: - nonResourceURLs: - /metrics verbs: - get --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-leader-election-rolebinding namespace: project-system roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: project-leader-election-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: project-manager-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: project-metrics-auth-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: project-metrics-auth-role subjects: - kind: ServiceAccount name: project-controller-manager namespace: project-system --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager-metrics-service namespace: project-system spec: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 selector: app.kubernetes.io/name: project control-plane: controller-manager --- apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-webhook-service namespace: project-system spec: ports: - port: 443 protocol: TCP targetPort: 9443 selector: app.kubernetes.io/name: project control-plane: controller-manager --- apiVersion: apps/v1 kind: Deployment metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager namespace: project-system spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: project control-plane: controller-manager template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: app.kubernetes.io/name: project control-plane: controller-manager spec: containers: - args: - --metrics-bind-address=:8443 - --leader-elect - --health-probe-bind-address=:8081 - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs command: - /manager image: controller:latest livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 name: manager ports: - containerPort: 9443 name: webhook-server protocol: TCP readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true volumeMounts: - mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true - mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs readOnly: true securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault serviceAccountName: project-controller-manager terminationGracePeriodSeconds: 10 volumes: - name: metrics-certs secret: items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key optional: false secretName: metrics-server-cert - name: webhook-certs secret: secretName: webhook-server-cert --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-metrics-certs namespace: project-system spec: dnsNames: - project-controller-manager-metrics-service.project-system.svc - project-controller-manager-metrics-service.project-system.svc.cluster.local issuerRef: kind: Issuer name: project-selfsigned-issuer secretName: metrics-server-cert --- apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-serving-cert namespace: project-system spec: dnsNames: - project-webhook-service.project-system.svc - project-webhook-service.project-system.svc.cluster.local issuerRef: kind: Issuer name: project-selfsigned-issuer secretName: webhook-server-cert --- apiVersion: cert-manager.io/v1 kind: Issuer metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project name: project-selfsigned-issuer namespace: project-system spec: selfSigned: {} --- apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: app.kubernetes.io/managed-by: kustomize app.kubernetes.io/name: project control-plane: controller-manager name: project-controller-manager-metrics-monitor namespace: project-system spec: endpoints: - bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token path: /metrics port: https scheme: https tlsConfig: ca: secret: key: ca.crt name: metrics-server-cert cert: secret: key: tls.crt name: metrics-server-cert insecureSkipVerify: false keySecret: key: tls.key name: metrics-server-cert serverName: project-controller-manager-metrics-service.project-system.svc selector: matchLabels: app.kubernetes.io/name: project control-plane: controller-manager --- apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: annotations: cert-manager.io/inject-ca-from: project-system/project-serving-cert name: project-mutating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: project-webhook-service namespace: project-system path: /mutate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: mcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None - admissionReviewVersions: - v1 clientConfig: service: name: project-webhook-service namespace: project-system path: /mutate-batch-tutorial-kubebuilder-io-v2-cronjob failurePolicy: Fail name: mcronjob-v2.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v2 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: annotations: cert-manager.io/inject-ca-from: project-system/project-serving-cert name: project-validating-webhook-configuration webhooks: - admissionReviewVersions: - v1 clientConfig: service: name: project-webhook-service namespace: project-system path: /validate-batch-tutorial-kubebuilder-io-v1-cronjob failurePolicy: Fail name: vcronjob-v1.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v1 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None - admissionReviewVersions: - v1 clientConfig: service: name: project-webhook-service namespace: project-system path: /validate-batch-tutorial-kubebuilder-io-v2-cronjob failurePolicy: Fail name: vcronjob-v2.kb.io rules: - apiGroups: - batch.tutorial.kubebuilder.io apiVersions: - v2 operations: - CREATE - UPDATE resources: - cronjobs sideEffects: None ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/go.mod ================================================ module tutorial.kubebuilder.io/project go 1.25.3 require ( github.com/onsi/ginkgo/v2 v2.27.2 github.com/onsi/gomega v1.38.2 github.com/robfig/cron v1.2.0 k8s.io/api v0.35.0 k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.23.3 ) require ( cel.dev/expr v0.24.0 // indirect github.com/Masterminds/semver/v3 v3.4.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.1 // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // 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-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/btree v1.1.3 // 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/pprof v0.0.0-20250403155104-27863c87afa6 // 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.0 // 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.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.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.29.0 // indirect golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.38.0 // indirect golang.org/x/term v0.37.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.9.0 // indirect golang.org/x/tools v0.38.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.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.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.0 // 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/kube-openapi v0.0.0-20250910181357-589584f1c912 // 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.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/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/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.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 v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 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-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 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.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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.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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-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/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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 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.1/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/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.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/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 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/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.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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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/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/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/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.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= 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-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.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/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 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/controller-runtime v0.23.3 h1:VjB/vhoPoA9l1kEKZHBMnQF33tdCLQKJtydy4iqwZ80= sigs.k8s.io/controller-runtime v0.23.3/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= 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.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/hack/boilerplate.go.txt ================================================ /* Copyright 2026 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: docs/book/src/multiversion-tutorial/testdata/project/internal/controller/cronjob_controller.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* We'll start out with some imports. You'll see below that we'll need a few more imports than those scaffolded for us. We'll talk about each one when we use it. */ package controller import ( "context" "fmt" "maps" "slices" "time" "github.com/robfig/cron" kbatch "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" 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" ref "k8s.io/client-go/tools/reference" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) /* Next, we'll need a Clock, which will allow us to fake timing in our tests. */ // CronJobReconciler reconciles a CronJob object type CronJobReconciler struct { client.Client Scheme *runtime.Scheme Clock } /* We'll mock out the clock to make it easier to jump around in time while testing, the "real" clock just calls `time.Now`. */ type realClock struct{} func (_ realClock) Now() time.Time { return time.Now() } //nolint:staticcheck // Clock knows how to get the current time. // It can be used to fake out timing for testing. type Clock interface { Now() time.Time } // +kubebuilder:docs-gen:collapse=Clock Code Implementation // Definitions to manage status conditions const ( // typeAvailableCronJob represents the status of the CronJob reconciliation typeAvailableCronJob = "Available" // typeProgressingCronJob represents the status used when the CronJob is being reconciled typeProgressingCronJob = "Progressing" // typeDegradedCronJob represents the status used when the CronJob has encountered an error typeDegradedCronJob = "Degraded" ) /* Notice that we need a few more RBAC permissions -- since we're creating and managing jobs now, we'll need permissions for those, which means adding a couple more [markers](/reference/markers/rbac.md). */ // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch // +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get /* Now, we get to the heart of the controller -- the reconciler logic. */ var ( scheduledTimeAnnotation = "batch.tutorial.kubebuilder.io/scheduled-at" ) // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // TODO(user): Modify the Reconcile function to compare the state specified by // the CronJob object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile // nolint:gocyclo func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logf.FromContext(ctx) /* ### 1: Load the CronJob by name We'll fetch the CronJob using our client. All client methods take a context (to allow for cancellation) as their first argument, and the object in question as their last. Get is a bit special, in that it takes a [`NamespacedName`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client?tab=doc#ObjectKey) as the middle argument (most don't have a middle argument, as we'll see below). Many client methods also take variadic options at the end. */ var cronJob batchv1.CronJob if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { if apierrors.IsNotFound(err) { // If the custom resource is not found then it usually means that it was deleted or not created // In this way, we will stop the reconciliation log.Info("CronJob resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get CronJob") return ctrl.Result{}, err } // Initialize status conditions if not yet present if len(cronJob.Status.Conditions) == 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation", }) if err := r.Status().Update(ctx, &cronJob); err != nil { log.Error(err, "Failed to update CronJob status") return ctrl.Result{}, err } /* After updating the status, we re-fetch the CronJob to ensure we are working with the latest version of the object from the API server. Kubernetes uses optimistic concurrency, meaning that any update (including a status update) may change the resource version. If we continue reconciliation with a stale copy, subsequent updates may fail with a conflict such as: "the object has been modified; please apply your changes to the latest version and try again". By re-fetching here, we keep our reconciliation logic in sync with the actual cluster state and avoid unnecessary conflicts and requeues. */ if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { log.Error(err, "Failed to re-fetch CronJob") return ctrl.Result{}, err } } /* ### 2: List all active jobs, and update the status To fully update our status, we'll need to list all child jobs in this namespace that belong to this CronJob. Similarly to Get, we can use the List method to list the child jobs. Notice that we use variadic options to set the namespace and field match (which is actually an index lookup that we set up below). */ var childJobs kbatch.JobList if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: req.Name}); err != nil { log.Error(err, "unable to list child Jobs") /* Before updating, ensure we have the latest state of the resource to avoid conflict errors (e.g. "the object has been modified") that would re-trigger the reconcile loop. */ if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "ReconciliationError", Message: fmt.Sprintf("Failed to list child jobs: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return ctrl.Result{}, err } /* Once we have all the jobs we own, we'll split them into active, successful, and failed jobs, keeping track of the most recent run so that we can record it in status. Remember, status should be able to be reconstituted from the state of the world, so it's generally not a good idea to read from the status of the root object. Instead, you should reconstruct it every run. That's what we'll do here. We can check if a job is "finished" and whether it succeeded or failed using status conditions. We'll put that logic in a helper to make our code cleaner. */ // find the active list of jobs var activeJobs []*kbatch.Job var successfulJobs []*kbatch.Job var failedJobs []*kbatch.Job var mostRecentTime *time.Time // find the last run so we can update the status /* We consider a job "finished" if it has a "Complete" or "Failed" condition marked as true. Status conditions allow us to add extensible status information to our objects that other humans and controllers can examine to check things like completion and health. */ isJobFinished := func(job *kbatch.Job) (bool, kbatch.JobConditionType) { for _, c := range job.Status.Conditions { if (c.Type == kbatch.JobComplete || c.Type == kbatch.JobFailed) && c.Status == corev1.ConditionTrue { return true, c.Type } } return false, "" } // +kubebuilder:docs-gen:collapse=isJobFinished /* We'll use a helper to extract the scheduled time from the annotation that we added during job creation. */ getScheduledTimeForJob := func(job *kbatch.Job) (*time.Time, error) { timeRaw := job.Annotations[scheduledTimeAnnotation] if len(timeRaw) == 0 { return nil, nil } timeParsed, err := time.Parse(time.RFC3339, timeRaw) if err != nil { return nil, err } return &timeParsed, nil } // +kubebuilder:docs-gen:collapse=getScheduledTimeForJob for i, job := range childJobs.Items { _, finishedType := isJobFinished(&job) switch finishedType { case "": // ongoing activeJobs = append(activeJobs, &childJobs.Items[i]) case kbatch.JobFailed: failedJobs = append(failedJobs, &childJobs.Items[i]) case kbatch.JobComplete: successfulJobs = append(successfulJobs, &childJobs.Items[i]) } // We'll store the launch time in an annotation, so we'll reconstitute that from // the active jobs themselves. scheduledTimeForJob, err := getScheduledTimeForJob(&job) if err != nil { log.Error(err, "unable to parse schedule time for child job", "job", &job) continue } if scheduledTimeForJob != nil { if mostRecentTime == nil || mostRecentTime.Before(*scheduledTimeForJob) { mostRecentTime = scheduledTimeForJob } } } if mostRecentTime != nil { cronJob.Status.LastScheduleTime = &metav1.Time{Time: *mostRecentTime} } else { cronJob.Status.LastScheduleTime = nil } cronJob.Status.Active = nil for _, activeJob := range activeJobs { jobRef, err := ref.GetReference(r.Scheme, activeJob) if err != nil { log.Error(err, "unable to make reference to active job", "job", activeJob) continue } cronJob.Status.Active = append(cronJob.Status.Active, *jobRef) } /* Here, we'll log how many jobs we observed at a slightly higher logging level, for debugging. Notice how instead of using a format string, we use a fixed message, and attach key-value pairs with the extra information. This makes it easier to filter and query log lines. */ log.V(1).Info("job count", "active jobs", len(activeJobs), "successful jobs", len(successfulJobs), "failed jobs", len(failedJobs)) // Check if CronJob is suspended isSuspended := cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend // Update status conditions based on current state if isSuspended { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionFalse, Reason: "Suspended", Message: "CronJob is suspended", }) } else if len(failedJobs) > 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "JobsFailed", Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)), }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionFalse, Reason: "JobsFailed", Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)), }) } else if len(activeJobs) > 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionTrue, Reason: "JobsActive", Message: fmt.Sprintf("%d job(s) are currently active", len(activeJobs)), }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionTrue, Reason: "JobsActive", Message: fmt.Sprintf("CronJob is progressing with %d active job(s)", len(activeJobs)), }) } else { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionTrue, Reason: "AllJobsCompleted", Message: "All jobs have completed successfully", }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionFalse, Reason: "NoJobsActive", Message: "No jobs are currently active", }) } /* Using the data we've gathered, we'll update the status of our CRD. Just like before, we use our client. To specifically update the status subresource, we'll use the `Status` part of the client, with the `Update` method. The status subresource ignores changes to spec, so it's less likely to conflict with any other updates, and can have separate permissions. */ if err := r.Status().Update(ctx, &cronJob); err != nil { log.Error(err, "unable to update CronJob status") return ctrl.Result{}, err } /* Once we've updated our status, we can move on to ensuring that the status of the world matches what we want in our spec. ### 3: Clean up old jobs according to the history limit First, we'll try to clean up old jobs, so that we don't leave too many lying around. */ // NB: deleting these are "best effort" -- if we fail on a particular one, // we won't requeue just to finish the deleting. if cronJob.Spec.FailedJobsHistoryLimit != nil { slices.SortStableFunc(failedJobs, func(a, b *kbatch.Job) int { aStartTime := a.Status.StartTime bStartTime := b.Status.StartTime if aStartTime == nil && bStartTime != nil { return 1 } if aStartTime.Before(bStartTime) { return -1 } else if bStartTime.Before(aStartTime) { return 1 } return 0 }) for i, job := range failedJobs { if int32(i) >= int32(len(failedJobs))-*cronJob.Spec.FailedJobsHistoryLimit { break } if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { log.Error(err, "unable to delete old failed job", "job", job) } else { log.V(1).Info("deleted old failed job", "job", job) } } } if cronJob.Spec.SuccessfulJobsHistoryLimit != nil { slices.SortStableFunc(successfulJobs, func(a, b *kbatch.Job) int { aStartTime := a.Status.StartTime bStartTime := b.Status.StartTime if aStartTime == nil && bStartTime != nil { return 1 } if aStartTime.Before(bStartTime) { return -1 } else if bStartTime.Before(aStartTime) { return 1 } return 0 }) for i, job := range successfulJobs { if int32(i) >= int32(len(successfulJobs))-*cronJob.Spec.SuccessfulJobsHistoryLimit { break } if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil { log.Error(err, "unable to delete old successful job", "job", job) } else { log.V(1).Info("deleted old successful job", "job", job) } } } /* ### 4: Check if we're suspended If this object is suspended, we don't want to run any jobs, so we'll stop now. This is useful if something's broken with the job we're running and we want to pause runs to investigate or putz with the cluster, without deleting the object. */ if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend { log.V(1).Info("cronjob suspended, skipping") return ctrl.Result{}, nil } /* ### 5: Get the next scheduled run If we're not paused, we'll need to calculate the next scheduled run, and whether or not we've got a run that we haven't processed yet. */ /* We'll calculate the next scheduled time using our helpful cron library. We'll start calculating appropriate times from our last run, or the creation of the CronJob if we can't find a last run. If there are too many missed runs and we don't have any deadlines set, we'll bail so that we don't cause issues on controller restarts or wedges. Otherwise, we'll just return the missed runs (of which we'll just use the latest), and the next run, so that we can know when it's time to reconcile again. */ getNextSchedule := func(cronJob *batchv1.CronJob, now time.Time) (lastMissed time.Time, next time.Time, err error) { sched, err := cron.ParseStandard(cronJob.Spec.Schedule) if err != nil { return time.Time{}, time.Time{}, fmt.Errorf("unparseable schedule %q: %w", cronJob.Spec.Schedule, err) } // for optimization purposes, cheat a bit and start from our last observed run time // we could reconstitute this here, but there's not much point, since we've // just updated it. var earliestTime time.Time if cronJob.Status.LastScheduleTime != nil { earliestTime = cronJob.Status.LastScheduleTime.Time } else { earliestTime = cronJob.CreationTimestamp.Time } if cronJob.Spec.StartingDeadlineSeconds != nil { // controller is not going to schedule anything below this point schedulingDeadline := now.Add(-time.Second * time.Duration(*cronJob.Spec.StartingDeadlineSeconds)) if schedulingDeadline.After(earliestTime) { earliestTime = schedulingDeadline } } if earliestTime.After(now) { return time.Time{}, sched.Next(now), nil } starts := 0 for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) { lastMissed = t // An object might miss several starts. For example, if // controller gets wedged on Friday at 5:01pm when everyone has // gone home, and someone comes in on Tuesday AM and discovers // the problem and restarts the controller, then all the hourly // jobs, more than 80 of them for one hourly scheduledJob, should // all start running with no further intervention (if the scheduledJob // allows concurrency and late starts). // // However, if there is a bug somewhere, or incorrect clock // on controller's server or apiservers (for setting creationTimestamp) // then there could be so many missed start times (it could be off // by decades or more), that it would eat up all the CPU and memory // of this controller. In that case, we want to not try to list // all the missed start times. starts++ if starts > 100 { // We can't get the most recent times so just return an empty slice return time.Time{}, time.Time{}, fmt.Errorf("Too many missed start times (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.") //nolint:staticcheck } } return lastMissed, sched.Next(now), nil } // +kubebuilder:docs-gen:collapse=getNextSchedule // figure out the next times that we need to create // jobs at (or anything we missed). missedRun, nextRun, err := getNextSchedule(&cronJob, r.Now()) if err != nil { log.Error(err, "unable to figure out CronJob schedule") if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the schedule error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "InvalidSchedule", Message: fmt.Sprintf("Failed to parse schedule: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } // we don't really care about requeuing until we get an update that // fixes the schedule, so don't return an error return ctrl.Result{}, nil } /* We'll prep our eventual request to requeue until the next job, and then figure out if we actually need to run. */ scheduledResult := ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())} // save this so we can re-use it elsewhere log = log.WithValues("now", r.Now(), "next run", nextRun) /* ### 6: Run a new job if it's on schedule, not past the deadline, and not blocked by our concurrency policy If we've missed a run, and we're still within the deadline to start it, we'll need to run a job. */ if missedRun.IsZero() { log.V(1).Info("no upcoming scheduled times, sleeping until next") return scheduledResult, nil } // make sure we're not too late to start the run log = log.WithValues("current run", missedRun) tooLate := false if cronJob.Spec.StartingDeadlineSeconds != nil { tooLate = missedRun.Add(time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second).Before(r.Now()) } if tooLate { log.V(1).Info("missed starting deadline for last run, sleeping till next") if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect missed deadline meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "MissedSchedule", Message: fmt.Sprintf("Missed starting deadline for run at %v", missedRun), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return scheduledResult, nil } /* If we actually have to run a job, we'll need to either wait till existing ones finish, replace the existing ones, or just add new ones. If our information is out of date due to cache delay, we'll get a requeue when we get up-to-date information. */ // figure out how to run this job -- concurrency policy might forbid us from running // multiple at the same time... if cronJob.Spec.ConcurrencyPolicy == batchv1.ForbidConcurrent && len(activeJobs) > 0 { log.V(1).Info("concurrency policy blocks concurrent runs, skipping", "num active", len(activeJobs)) return scheduledResult, nil } // ...or instruct us to replace existing ones... if cronJob.Spec.ConcurrencyPolicy == batchv1.ReplaceConcurrent { for _, activeJob := range activeJobs { // we don't care if the job was already deleted if err := r.Delete(ctx, activeJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { log.Error(err, "unable to delete active job", "job", activeJob) return ctrl.Result{}, err } } } /* Once we've figured out what to do with existing jobs, we'll actually create our desired job */ /* We need to construct a job based on our CronJob's template. We'll copy over the spec from the template and copy some basic object meta. Then, we'll set the "scheduled time" annotation so that we can reconstitute our `LastScheduleTime` field each reconcile. Finally, we'll need to set an owner reference. This allows the Kubernetes garbage collector to clean up jobs when we delete the CronJob, and allows controller-runtime to figure out which cronjob needs to be reconciled when a given job changes (is added, deleted, completes, etc). */ constructJobForCronJob := func(cronJob *batchv1.CronJob, scheduledTime time.Time) (*kbatch.Job, error) { // We want job names for a given nominal start time to have a deterministic name to avoid the same job being created twice name := fmt.Sprintf("%s-%d", cronJob.Name, scheduledTime.Unix()) job := &kbatch.Job{ ObjectMeta: metav1.ObjectMeta{ Labels: make(map[string]string), Annotations: make(map[string]string), Name: name, Namespace: cronJob.Namespace, }, Spec: *cronJob.Spec.JobTemplate.Spec.DeepCopy(), } maps.Copy(job.Annotations, cronJob.Spec.JobTemplate.Annotations) job.Annotations[scheduledTimeAnnotation] = scheduledTime.Format(time.RFC3339) maps.Copy(job.Labels, cronJob.Spec.JobTemplate.Labels) if err := ctrl.SetControllerReference(cronJob, job, r.Scheme); err != nil { return nil, err } return job, nil } // +kubebuilder:docs-gen:collapse=constructJobForCronJob // actually make the job... job, err := constructJobForCronJob(&cronJob, missedRun) if err != nil { log.Error(err, "unable to construct job from template") // don't bother requeuing until we get a change to the spec return scheduledResult, nil } // ...and create it on the cluster if err := r.Create(ctx, job); err != nil { log.Error(err, "unable to create Job for CronJob", "job", job) if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "JobCreationFailed", Message: fmt.Sprintf("Failed to create job: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return ctrl.Result{}, err } log.V(1).Info("created Job for CronJob run", "job", job) if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect successful job creation meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionTrue, Reason: "JobCreated", Message: fmt.Sprintf("Created job %s", job.Name), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } /* ### 7: Requeue when we either see a running job or it's time for the next scheduled run Finally, we'll return the result that we prepped above, that says we want to requeue when our next run would need to occur. This is taken as a maximum deadline -- if something else changes in between, like our job starts or finishes, we get modified, etc, we might reconcile again sooner. */ // we'll requeue once we see the running job, and update our status return scheduledResult, nil } /* ### Setup Finally, we'll update our setup. In order to allow our reconciler to quickly look up Jobs by their owner, we'll need an index. We declare an index key that we can later use with the client as a pseudo-field name, and then describe how to extract the indexed value from the Job object. The indexer will automatically take care of namespaces for us, so we just have to extract the owner name if the Job has a CronJob owner. Additionally, we'll inform the manager that this controller owns some Jobs, so that it will automatically call Reconcile on the underlying CronJob when a Job changes, is deleted, etc. */ var ( jobOwnerKey = ".metadata.controller" apiGVStr = batchv1.GroupVersion.String() ) // SetupWithManager sets up the controller with the Manager. func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error { // set up a real clock, since we're not in a test if r.Clock == nil { r.Clock = realClock{} } if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kbatch.Job{}, jobOwnerKey, func(rawObj client.Object) []string { // grab the job object, extract the owner... job := rawObj.(*kbatch.Job) owner := metav1.GetControllerOf(job) if owner == nil { return nil } // ...make sure it's a CronJob... if owner.APIVersion != apiGVStr || owner.Kind != "CronJob" { return nil } // ...and if so, return it return []string{owner.Name} }); err != nil { return err } return ctrl.NewControllerManagedBy(mgr). For(&batchv1.CronJob{}). Owns(&kbatch.Job{}). Named("cronjob"). Complete(r) } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/controller/cronjob_controller_test.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package controller import ( "context" "fmt" "reflect" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" cronjobv1 "tutorial.kubebuilder.io/project/api/v1" ) var _ = Describe("CronJob controller", func() { Context("CronJob controller test", func() { const NamespaceName = "test-cronjob" ctx := context.Background() namespace := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: NamespaceName, Namespace: NamespaceName, }, } SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) BeforeEach(func() { By("Creating the Namespace to perform the tests") err := k8sClient.Get(ctx, types.NamespacedName{Name: NamespaceName}, &v1.Namespace{}) if err != nil && errors.IsNotFound(err) { err = k8sClient.Create(ctx, namespace) Expect(err).NotTo(HaveOccurred()) } }) AfterEach(func() { // Note: We don't delete the namespace here to avoid issues with parallel test execution. // The namespace will be cleaned up when the test suite finishes. }) It("should initialize status conditions on first reconciliation", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Checking that status conditions are initialized") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Conditions).NotTo(BeEmpty()) }).Should(Succeed()) By("Cleaning up the CronJob") Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should set AllJobsCompleted condition when no active jobs exist", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Checking that the CronJob has zero active Jobs") Consistently(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(BeEmpty()) }).WithTimeout(time.Second * 5).WithPolling(time.Millisecond * 250).Should(Succeed()) By("Checking AllJobsCompleted condition") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) if len(availableConditions) > 0 { Expect(availableConditions[0].Status).To(Equal(metav1.ConditionTrue)) Expect(availableConditions[0].Reason).To(Equal("AllJobsCompleted")) } var progressingConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Progressing")), &progressingConditions)) if len(progressingConditions) > 0 { Expect(progressingConditions[0].Status).To(Equal(metav1.ConditionFalse)) Expect(progressingConditions[0].Reason).To(Equal("NoJobsActive")) } By("Cleaning up the CronJob") Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should track active jobs and set JobsActive condition", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Creating an active Job owned by the CronJob") testJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("test-job-%d", GinkgoRandomSeed()), Namespace: NamespaceName, }, Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, } kind := reflect.TypeFor[cronjobv1.CronJob]().Name() gvk := cronjobv1.GroupVersion.WithKind(kind) Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) }).Should(Succeed()) controllerRef := metav1.NewControllerRef(cronJob, gvk) testJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) Expect(k8sClient.Create(ctx, testJob)).To(Succeed()) testJob.Status.Active = 2 Expect(k8sClient.Status().Update(ctx, testJob)).To(Succeed()) By("Checking that the CronJob has one active Job in status") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(HaveLen(1)) g.Expect(cronJob.Status.Active[0].Name).To(Equal(testJob.Name)) }).Should(Succeed()) By("Checking JobsActive conditions") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) Expect(availableConditions).To(HaveLen(1)) Expect(availableConditions[0].Status).To(Equal(metav1.ConditionTrue)) Expect(availableConditions[0].Reason).To(Equal("JobsActive")) var progressingConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Progressing")), &progressingConditions)) Expect(progressingConditions).To(HaveLen(1)) Expect(progressingConditions[0].Status).To(Equal(metav1.ConditionTrue)) Expect(progressingConditions[0].Reason).To(Equal("JobsActive")) By("Cleaning up") Expect(k8sClient.Delete(ctx, testJob)).To(Succeed()) Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should set Degraded condition when jobs fail", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Creating a failed Job owned by the CronJob") failedJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("test-job-failed-%d", GinkgoRandomSeed()), Namespace: NamespaceName, }, Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, } kind := reflect.TypeFor[cronjobv1.CronJob]().Name() gvk := cronjobv1.GroupVersion.WithKind(kind) Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) }).Should(Succeed()) controllerRef := metav1.NewControllerRef(cronJob, gvk) failedJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) Expect(k8sClient.Create(ctx, failedJob)).To(Succeed()) now := metav1.Now() failedJob.Status.StartTime = &now failedJob.Status.Conditions = append(failedJob.Status.Conditions, batchv1.JobCondition{ Type: batchv1.JobFailureTarget, Status: v1.ConditionTrue, }, batchv1.JobCondition{ Type: batchv1.JobFailed, Status: v1.ConditionTrue, }) Expect(k8sClient.Status().Update(ctx, failedJob)).To(Succeed()) By("Checking that Degraded=True when jobs fail") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var degradedConditions []metav1.Condition g.Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Degraded")), °radedConditions)) if len(degradedConditions) > 0 { g.Expect(degradedConditions[0].Status).To(Equal(metav1.ConditionTrue)) g.Expect(degradedConditions[0].Reason).To(Equal("JobsFailed")) } }).Should(Succeed()) By("Checking that Available=False when jobs fail") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) if len(availableConditions) > 0 { Expect(availableConditions[0].Status).To(Equal(metav1.ConditionFalse)) Expect(availableConditions[0].Reason).To(Equal("JobsFailed")) } By("Cleaning up") Expect(k8sClient.Delete(ctx, failedJob)).To(Succeed()) Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should set Available=False when CronJob is suspended", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Updating the CronJob to suspend it") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) cronJob.Spec.Suspend = ptr.To(true) g.Expect(k8sClient.Update(ctx, cronJob)).To(Succeed()) }).Should(Succeed()) By("Checking that Available=False when suspended") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition g.Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) if len(availableConditions) > 0 { g.Expect(availableConditions[0].Status).To(Equal(metav1.ConditionFalse)) g.Expect(availableConditions[0].Reason).To(Equal("Suspended")) } }).Should(Succeed()) By("Cleaning up the CronJob") Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) }) }) ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/controller/suite_test.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* When we created the CronJob API with `kubebuilder create api` in a [previous chapter](/cronjob-tutorial/new-api.md), Kubebuilder already did some test work for you. Kubebuilder scaffolded a `internal/controller/suite_test.go` file that does the bare bones of setting up a test environment. First, it will contain the necessary imports. */ package controller import ( "context" "os" "path/filepath" "testing" "time" ctrl "sigs.k8s.io/controller-runtime" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" batchv1 "tutorial.kubebuilder.io/project/api/v1" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. // +kubebuilder:docs-gen:collapse=Imports /* Now, let's go through the code generated. */ var ( ctx context.Context cancel context.CancelFunc testEnv *envtest.Environment cfg *rest.Config k8sClient client.Client // You'll be using this client in your tests. ) func TestControllers(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Controller Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) var err error /* The CronJob Kind is added to the runtime scheme used by the test environment. This ensures that the CronJob API is registered with the scheme, allowing the test controller to recognize and interact with CronJob resources. */ err = batchv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) /* After the schemas, you will see the following marker. This marker is what allows new schemas to be added here automatically when a new API is added to the project. */ // +kubebuilder:scaffold:scheme /* The envtest environment is configured to load Custom Resource Definitions (CRDs) from the specified directory. This setup enables the test environment to recognize and interact with the custom resources defined by these CRDs. */ By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: true, } // Retrieve the first found binary directory to allow running tests from IDEs if getFirstFoundEnvTestBinaryDir() != "" { testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() } /* Then, we start the envtest cluster. */ // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) /* A client is created for our test CRUD operations. */ k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) /* One thing that this autogenerated file is missing, however, is a way to actually start your controller. The code above will set up a client for interacting with your custom Kind, but will not be able to test your controller behavior. If you want to test your custom controller logic, you’ll need to add some familiar-looking manager logic to your BeforeSuite() function, so you can register your custom controller to run on this test cluster. You may notice that the code below runs your controller with nearly identical logic to your CronJob project’s main.go! The only difference is that the manager is started in a separate goroutine so it does not block the cleanup of envtest when you’re done running your tests. Note that we set up both a "live" k8s client and a separate client from the manager. This is because when making assertions in tests, you generally want to assert against the live state of the API server. If you use the client from the manager (`k8sManager.GetClient`), you'd end up asserting against the contents of the cache instead, which is slower and can introduce flakiness into your tests. We could use the manager's `APIReader` to accomplish the same thing, but that would leave us with two clients in our test assertions and setup (one for reading, one for writing), and it'd be easy to make mistakes. Note that we keep the reconciler running against the manager's cache client, though -- we want our controller to behave as it would in production, and we use features of the cache (like indices) in our controller which aren't available when talking directly to the API server. */ k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, }) Expect(err).ToNot(HaveOccurred()) err = (&CronJobReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) Expect(err).ToNot(HaveOccurred(), "failed to run manager") }() }) /* Kubebuilder also generates boilerplate functions for cleaning up envtest and actually running your test files in your controllers/ directory. You won't need to touch these. */ var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) /* Now that you have your controller running on a test cluster and a client ready to perform operations on your CronJob, we can start writing integration tests! */ // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. // ENVTEST-based tests depend on specific binaries, usually located in paths set by // controller-runtime. When running tests directly (e.g., via an IDE) without using // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. // // This function streamlines the process by finding the required binaries, similar to // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are // properly set up, run 'make setup-envtest' beforehand. func getFirstFoundEnvTestBinaryDir() string { basePath := filepath.Join("..", "..", "bin", "k8s") entries, err := os.ReadDir(basePath) if err != nil { logf.Log.Error(err, "Failed to read directory", "path", basePath) return "" } for _, entry := range entries { if entry.IsDir() { return filepath.Join(basePath, entry.Name()) } } return "" } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package v1 import ( "context" "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Imports /* Next, we'll setup a logger for the webhooks. */ var cronjoblog = logf.Log.WithName("cronjob-resource") /* This setup doubles as setup for our conversion webhooks: as long as our types implement the [Hub](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub) and [Convertible](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) interfaces, a conversion webhook will be registered. */ // SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr, &batchv1.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, }). Complete() } /* Notice that we use kubebuilder markers to generate webhook manifests. This marker is responsible for generating a mutating webhook manifest. The meaning of each marker can be found [here](/reference/markers/webhook.md). */ /* This marker is responsible for generating a mutation webhook manifest. */ // +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob-v1.kb.io,admissionReviewVersions=v1 // CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind CronJob when those are created or updated. // // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as it is used only for temporary operations and does not need to be deeply copied. type CronJobCustomDefaulter struct { // Default values for various CronJob fields DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 } /* We use the `webhook.CustomDefaulter`interface to set defaults to our CRD. A webhook will automatically be served that calls this defaulting. The `Default`method is expected to mutate the receiver, setting the defaults. */ // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. func (d *CronJobCustomDefaulter) Default(_ context.Context, obj *batchv1.CronJob) error { cronjoblog.Info("Defaulting for CronJob", "name", obj.GetName()) // Set default values d.applyDefaults(obj) return nil } // applyDefaults applies default values to CronJob fields. func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) { if cronJob.Spec.ConcurrencyPolicy == "" { cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } if cronJob.Spec.Suspend == nil { cronJob.Spec.Suspend = new(bool) *cronJob.Spec.Suspend = d.DefaultSuspend } if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } if cronJob.Spec.FailedJobsHistoryLimit == nil { cronJob.Spec.FailedJobsHistoryLimit = new(int32) *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } /* We can validate our CRD beyond what's possible with declarative validation. Generally, declarative validation should be sufficient, but sometimes more advanced use cases call for complex validation. For instance, we'll see below that we use this to validate a well-formed cron schedule without making up a long regular expression. If `webhook.CustomValidator` interface is implemented, a webhook will automatically be served that calls the validation. The `ValidateCreate`, `ValidateUpdate` and `ValidateDelete` methods are expected to validate its receiver upon creation, update and deletion respectively. We separate out ValidateCreate from ValidateUpdate to allow behavior like making certain fields immutable, so that they can only be set on creation. ValidateDelete is also separated from ValidateUpdate to allow different validation behavior on deletion. Here, however, we just use the same shared validation for `ValidateCreate` and `ValidateUpdate`. And we do nothing in `ValidateDelete`, since we don't need to validate anything on deletion. */ /* This marker is responsible for generating a validation webhook manifest. */ // NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'. // +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1 // CronJobCustomValidator struct is responsible for validating the CronJob resource // when it is created, updated, or deleted. // // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as this struct is used only for temporary operations and does not need to be deeply copied. type CronJobCustomValidator struct { // +kubebuilder:docs-gen:collapse=Remaining Webhook Code // TODO(user): Add more fields as needed for validation } // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon creation", "name", obj.GetName()) return nil, validateCronJob(obj) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *batchv1.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon update", "name", newObj.GetName()) return nil, validateCronJob(newObj) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon deletion", "name", obj.GetName()) // TODO(user): fill in your validation logic upon object deletion. return nil, nil } /* We validate the name and the spec of the CronJob. */ // validateCronJob validates the fields of a CronJob object. func validateCronJob(cronjob *batchv1.CronJob) error { var allErrs field.ErrorList if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil } return apierrors.NewInvalid( schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs) } /* Some fields are declaratively validated by OpenAPI schema. You can find kubebuilder validation markers (prefixed with `// +kubebuilder:validation`) in the [Designing an API](api-design.md) section. You can find all of the kubebuilder supported markers for declaring validation by running `controller-gen crd -w`, or [here](/reference/markers/crd-validation.md). */ func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error { // The field helpers from the kubernetes API machinery help us return nicely // structured validation errors. return validateScheduleFormat( cronjob.Spec.Schedule, field.NewPath("spec").Child("schedule")) } /* We'll need to validate the [cron](https://en.wikipedia.org/wiki/Cron) schedule is well-formatted. */ func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error { if _, err := cron.ParseStandard(schedule); err != nil { return field.Invalid(fldPath, schedule, err.Error()) } return nil } /* Validating the length of a string field can be done declaratively by the validation schema. But the `ObjectMeta.Name` field is defined in a shared package under the apimachinery repo, so we can't declaratively validate it using the validation schema. */ func validateCronJobName(cronjob *batchv1.CronJob) *field.Error { if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 { // The job name length is 63 characters like all Kubernetes objects // (which must fit in a DNS subdomain). The cronjob controller appends // a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating // a job. The job name length limit is 63 characters. Therefore cronjob // names must have length <= 63-11=52. If we don't validate this here, // then job creation will fail later. return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters") } return nil } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/cronjob_webhook_test.go ================================================ /* Copyright 2026 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 v1 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" batchv1 "tutorial.kubebuilder.io/project/api/v1" // TODO (user): Add any additional imports if needed "k8s.io/utils/ptr" ) var _ = Describe("CronJob Webhook", func() { var ( obj *batchv1.CronJob oldObj *batchv1.CronJob validator CronJobCustomValidator defaulter CronJobCustomDefaulter ) const validCronJobName = "valid-cronjob-name" const schedule = "*/5 * * * *" BeforeEach(func() { obj = &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: schedule, ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: ptr.To(int32(3)), FailedJobsHistoryLimit: ptr.To(int32(1)), }, } *obj.Spec.SuccessfulJobsHistoryLimit = 3 *obj.Spec.FailedJobsHistoryLimit = 1 oldObj = &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: schedule, ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: ptr.To(int32(3)), FailedJobsHistoryLimit: ptr.To(int32(1)), }, } *oldObj.Spec.SuccessfulJobsHistoryLimit = 3 *oldObj.Spec.FailedJobsHistoryLimit = 1 validator = CronJobCustomValidator{} defaulter = CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, } Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") }) AfterEach(func() { // TODO (user): Add any teardown logic common to all tests }) Context("When creating CronJob under Defaulting Webhook", func() { It("Should apply defaults when a required field is empty", func() { By("simulating a scenario where defaults should be applied") obj.Spec.ConcurrencyPolicy = "" // This should default to AllowConcurrent obj.Spec.Suspend = nil // This should default to false obj.Spec.SuccessfulJobsHistoryLimit = nil // This should default to 3 obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1 By("calling the Default method to apply defaults") _ = defaulter.Default(ctx, obj) By("checking that the default values are set") Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") Expect(*obj.Spec.Suspend).To(BeFalse(), "Expected Suspend to default to false") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(3)), "Expected SuccessfulJobsHistoryLimit to default to 3") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(1)), "Expected FailedJobsHistoryLimit to default to 1") }) It("Should not overwrite fields that are already set", func() { By("setting fields that would normally get a default") obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent obj.Spec.Suspend = ptr.To(true) obj.Spec.SuccessfulJobsHistoryLimit = ptr.To(int32(5)) obj.Spec.FailedJobsHistoryLimit = ptr.To(int32(2)) By("calling the Default method to apply defaults") _ = defaulter.Default(ctx, obj) By("checking that the fields were not overwritten") Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") Expect(obj.Spec.Suspend).NotTo(BeNil()) Expect(*obj.Spec.Suspend).To(BeTrue(), "Expected Suspend to retain its set value") Expect(obj.Spec.SuccessfulJobsHistoryLimit).NotTo(BeNil()) Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(5)), "Expected SuccessfulJobsHistoryLimit to retain its set value") Expect(obj.Spec.FailedJobsHistoryLimit).NotTo(BeNil()) Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(2)), "Expected FailedJobsHistoryLimit to retain its set value") }) }) Context("When creating or updating CronJob under Validating Webhook", func() { It("Should deny creation if the name is too long", func() { obj.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" Expect(validator.ValidateCreate(ctx, obj)).Error().To( MatchError(ContainSubstring("must be no more than 52 characters")), "Expected name validation to fail for a too-long name") }) It("Should admit creation if the name is valid", func() { obj.Name = validCronJobName Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), "Expected name validation to pass for a valid name") }) It("Should deny creation if the schedule is invalid", func() { obj.Spec.Schedule = "invalid-cron-schedule" Expect(validator.ValidateCreate(ctx, obj)).Error().To( MatchError(ContainSubstring("Expected exactly 5 fields, found 1: invalid-cron-schedule")), "Expected spec validation to fail for an invalid schedule") }) It("Should admit creation if the schedule is valid", func() { obj.Spec.Schedule = schedule Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), "Expected spec validation to pass for a valid schedule") }) It("Should deny update if both name and spec are invalid", func() { oldObj.Name = validCronJobName oldObj.Spec.Schedule = schedule By("simulating an update") obj.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" obj.Spec.Schedule = "invalid-cron-schedule" By("validating an update") Expect(validator.ValidateUpdate(ctx, oldObj, obj)).Error().To(HaveOccurred(), "Expected validation to fail for both name and spec") }) It("Should admit update if both name and spec are valid", func() { oldObj.Name = validCronJobName oldObj.Spec.Schedule = schedule By("simulating an update") obj.Name = "valid-cronjob-name-updated" obj.Spec.Schedule = "0 0 * * *" By("validating an update") Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil(), "Expected validation to pass for a valid update") }) }) Context("When creating CronJob under Conversion Webhook", func() { // TODO (user): Add logic to convert the object to the desired version and verify the conversion // Example: // It("Should convert the object correctly", func() { // convertedObj := &batchv1.CronJob{} // Expect(obj.ConvertTo(convertedObj)).To(Succeed()) // Expect(convertedObj).ToNot(BeNil()) // }) }) }) ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v1/webhook_suite_test.go ================================================ /* Copyright 2026 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 v1 import ( "context" "crypto/tls" "fmt" "net" "os" "path/filepath" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" batchv1 "tutorial.kubebuilder.io/project/api/v1" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var ( ctx context.Context cancel context.CancelFunc k8sClient client.Client cfg *rest.Config testEnv *envtest.Environment ) func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) var err error err = batchv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: false, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, }, } // Retrieve the first found binary directory to allow running tests from IDEs if getFirstFoundEnvTestBinaryDir() != "" { testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() } // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) // start webhook server using Manager. webhookInstallOptions := &testEnv.WebhookInstallOptions mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, WebhookServer: webhook.NewServer(webhook.Options{ Host: webhookInstallOptions.LocalServingHost, Port: webhookInstallOptions.LocalServingPort, CertDir: webhookInstallOptions.LocalServingCertDir, }), LeaderElection: false, Metrics: metricsserver.Options{BindAddress: "0"}, }) Expect(err).NotTo(HaveOccurred()) err = SetupCronJobWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:webhook go func() { defer GinkgoRecover() err = mgr.Start(ctx) Expect(err).NotTo(HaveOccurred()) }() // wait for the webhook server to get ready. dialer := &net.Dialer{Timeout: time.Second} addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) Eventually(func() error { conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) if err != nil { return err } return conn.Close() }).Should(Succeed()) }) var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. // ENVTEST-based tests depend on specific binaries, usually located in paths set by // controller-runtime. When running tests directly (e.g., via an IDE) without using // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. // // This function streamlines the process by finding the required binaries, similar to // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are // properly set up, run 'make setup-envtest' beforehand. func getFirstFoundEnvTestBinaryDir() string { basePath := filepath.Join("..", "..", "..", "bin", "k8s") entries, err := os.ReadDir(basePath) if err != nil { logf.Log.Error(err, "Failed to read directory", "path", basePath) return "" } for _, entry := range entries { if entry.IsDir() { return filepath.Join(basePath, entry.Name()) } } return "" } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook.go ================================================ /* Copyright 2026 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. */ // +kubebuilder:docs-gen:collapse=Apache License package v2 import ( "context" "strings" "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" ctrl "sigs.k8s.io/controller-runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" batchv2 "tutorial.kubebuilder.io/project/api/v2" ) // +kubebuilder:docs-gen:collapse=Imports // nolint:unused // log is for logging in this package. var cronjoblog = logf.Log.WithName("cronjob-resource") // SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr, &batchv2.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv2.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, }). Complete() } // TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v2-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v2,name=mcronjob-v2.kb.io,admissionReviewVersions=v1 // CronJobCustomDefaulter struct is responsible for setting default values on the custom resource of the // Kind CronJob when those are created or updated. // // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as it is used only for temporary operations and does not need to be deeply copied. type CronJobCustomDefaulter struct { // Default values for various CronJob fields DefaultConcurrencyPolicy batchv2.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 } // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob. func (d *CronJobCustomDefaulter) Default(_ context.Context, obj *batchv2.CronJob) error { cronjoblog.Info("Defaulting for CronJob", "name", obj.GetName()) // Set default values d.applyDefaults(obj) return nil } // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. // NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'. // +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v2-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v2,name=vcronjob-v2.kb.io,admissionReviewVersions=v1 // CronJobCustomValidator struct is responsible for validating the CronJob resource // when it is created, updated, or deleted. // // NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as this struct is used only for temporary operations and does not need to be deeply copied. type CronJobCustomValidator struct { // TODO(user): Add more fields as needed for validation } // ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateCreate(_ context.Context, obj *batchv2.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon creation", "name", obj.GetName()) return nil, validateCronJob(obj) } // ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *batchv2.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon update", "name", newObj.GetName()) return nil, validateCronJob(newObj) } // ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob. func (v *CronJobCustomValidator) ValidateDelete(_ context.Context, obj *batchv2.CronJob) (admission.Warnings, error) { cronjoblog.Info("Validation for CronJob upon deletion", "name", obj.GetName()) // TODO(user): fill in your validation logic upon object deletion. return nil, nil } // applyDefaults applies default values to CronJob fields. func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv2.CronJob) { if cronJob.Spec.ConcurrencyPolicy == "" { cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } if cronJob.Spec.Suspend == nil { cronJob.Spec.Suspend = new(bool) *cronJob.Spec.Suspend = d.DefaultSuspend } if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } if cronJob.Spec.FailedJobsHistoryLimit == nil { cronJob.Spec.FailedJobsHistoryLimit = new(int32) *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } // +kubebuilder:docs-gen:collapse=Webhook Setup and Defaulting // validateCronJob validates the fields of a CronJob object. func validateCronJob(cronjob *batchv2.CronJob) error { var allErrs field.ErrorList if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil } return apierrors.NewInvalid(schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs) } func validateCronJobName(cronjob *batchv2.CronJob) *field.Error { if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 { return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters") } return nil } // validateCronJobSpec validates the schedule format of the custom CronSchedule type func validateCronJobSpec(cronjob *batchv2.CronJob) *field.Error { // Build cron expression from the parts parts := []string{"*", "*", "*", "*", "*"} // default parts for minute, hour, day of month, month, day of week if cronjob.Spec.Schedule.Minute != nil { parts[0] = string(*cronjob.Spec.Schedule.Minute) // Directly cast CronField (which is an alias of string) to string } if cronjob.Spec.Schedule.Hour != nil { parts[1] = string(*cronjob.Spec.Schedule.Hour) } if cronjob.Spec.Schedule.DayOfMonth != nil { parts[2] = string(*cronjob.Spec.Schedule.DayOfMonth) } if cronjob.Spec.Schedule.Month != nil { parts[3] = string(*cronjob.Spec.Schedule.Month) } if cronjob.Spec.Schedule.DayOfWeek != nil { parts[4] = string(*cronjob.Spec.Schedule.DayOfWeek) } // Join parts to form the full cron expression cronExpression := strings.Join(parts, " ") return validateScheduleFormat( cronExpression, field.NewPath("spec").Child("schedule")) } func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error { if _, err := cron.ParseStandard(schedule); err != nil { return field.Invalid(fldPath, schedule, "invalid cron schedule format: "+err.Error()) } return nil } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/cronjob_webhook_test.go ================================================ /* Copyright 2026 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 v2 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" batchv2 "tutorial.kubebuilder.io/project/api/v2" // TODO (user): Add any additional imports if needed ) var _ = Describe("CronJob Webhook", func() { var ( obj *batchv2.CronJob oldObj *batchv2.CronJob validator CronJobCustomValidator defaulter CronJobCustomDefaulter ) BeforeEach(func() { obj = &batchv2.CronJob{} oldObj = &batchv2.CronJob{} validator = CronJobCustomValidator{} Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") defaulter = CronJobCustomDefaulter{} Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") }) AfterEach(func() { // TODO (user): Add any teardown logic common to all tests }) Context("When creating CronJob under Defaulting Webhook", func() { // TODO (user): Add logic for defaulting webhooks // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" // By("calling the Default method to apply defaults") // defaulter.Default(ctx, obj) // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // }) }) Context("When creating or updating CronJob under Validating Webhook", func() { // TODO (user): Add logic for validating webhooks // Example: // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) // }) }) }) ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/internal/webhook/v2/webhook_suite_test.go ================================================ /* Copyright 2026 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 v2 import ( "context" "crypto/tls" "fmt" "net" "os" "path/filepath" "testing" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" batchv2 "tutorial.kubebuilder.io/project/api/v2" // +kubebuilder:scaffold:imports ) // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. var ( ctx context.Context cancel context.CancelFunc k8sClient client.Client cfg *rest.Config testEnv *envtest.Environment ) func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Webhook Suite") } var _ = BeforeSuite(func() { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) ctx, cancel = context.WithCancel(context.TODO()) var err error err = batchv2.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme By("bootstrapping test environment") testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, ErrorIfCRDPathMissing: false, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, }, } // Retrieve the first found binary directory to allow running tests from IDEs if getFirstFoundEnvTestBinaryDir() != "" { testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() } // cfg is defined in this file globally. cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) // start webhook server using Manager. webhookInstallOptions := &testEnv.WebhookInstallOptions mgr, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, WebhookServer: webhook.NewServer(webhook.Options{ Host: webhookInstallOptions.LocalServingHost, Port: webhookInstallOptions.LocalServingPort, CertDir: webhookInstallOptions.LocalServingCertDir, }), LeaderElection: false, Metrics: metricsserver.Options{BindAddress: "0"}, }) Expect(err).NotTo(HaveOccurred()) err = SetupCronJobWebhookWithManager(mgr) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:webhook go func() { defer GinkgoRecover() err = mgr.Start(ctx) Expect(err).NotTo(HaveOccurred()) }() // wait for the webhook server to get ready. dialer := &net.Dialer{Timeout: time.Second} addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) Eventually(func() error { conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) if err != nil { return err } return conn.Close() }).Should(Succeed()) }) var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) // getFirstFoundEnvTestBinaryDir locates the first binary in the specified path. // ENVTEST-based tests depend on specific binaries, usually located in paths set by // controller-runtime. When running tests directly (e.g., via an IDE) without using // Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured. // // This function streamlines the process by finding the required binaries, similar to // setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are // properly set up, run 'make setup-envtest' beforehand. func getFirstFoundEnvTestBinaryDir() string { basePath := filepath.Join("..", "..", "..", "bin", "k8s") entries, err := os.ReadDir(basePath) if err != nil { logf.Log.Error(err, "Failed to read directory", "path", basePath) return "" } for _, entry := range entries { if entry.IsDir() { return filepath.Join(basePath, entry.Name()) } } return "" } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/test/e2e/e2e_suite_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2026 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 e2e import ( "fmt" "os" "os/exec" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "tutorial.kubebuilder.io/project/test/utils" ) var ( // managerImage is the manager image to be built and loaded for testing. managerImage = "example.com/project:v0.0.1" // shouldCleanupCertManager tracks whether CertManager was installed by this suite. shouldCleanupCertManager = false // shouldCleanupPrometheus tracks whether Prometheus was installed by this suite. shouldCleanupPrometheus = false ) // TestE2E runs the e2e test suite to validate the solution in an isolated environment. // The default setup requires Kind and CertManager. // // To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true func TestE2E(t *testing.T) { RegisterFailHandler(Fail) _, _ = fmt.Fprintf(GinkgoWriter, "Starting project e2e test suite\n") RunSpecs(t, "e2e suite") } var _ = BeforeSuite(func() { By("Ensure that Prometheus is enabled") _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") By("building the manager image") cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage)) _, err := utils.Run(cmd) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image") // TODO(user): If you want to change the e2e test vendor from Kind, // ensure the image is built and available, then remove the following block. By("loading the manager image on Kind") err = utils.LoadImageToKindClusterWithName(managerImage) ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind") setupCertManager() By("checking if Prometheus is already installed") if !utils.IsPrometheusCRDsInstalled() { // Mark for cleanup before installation to handle interruptions and partial installs. shouldCleanupPrometheus = true By("installing Prometheus Operator") Expect(utils.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") } }) var _ = AfterSuite(func() { // Teardown Prometheus if it was installed by this suite if shouldCleanupPrometheus { By("uninstalling Prometheus Operator") utils.UninstallPrometheusOperator() } teardownCertManager() }) // setupCertManager installs CertManager if needed for webhook tests. // Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present. func setupCertManager() { if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" { _, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n") return } By("checking if CertManager is already installed") if utils.IsCertManagerCRDsInstalled() { _, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n") return } // Mark for cleanup before installation to handle interruptions and partial installs. shouldCleanupCertManager = true By("installing CertManager") Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager") } // teardownCertManager uninstalls CertManager if it was installed by setupCertManager. // This ensures we only remove what we installed. func teardownCertManager() { if !shouldCleanupCertManager { _, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n") return } By("uninstalling CertManager") utils.UninstallCertManager() } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/test/e2e/e2e_test.go ================================================ //go:build e2e // +build e2e /* Copyright 2026 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 e2e import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "time" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "tutorial.kubebuilder.io/project/test/utils" ) // namespace where the project is deployed in const namespace = "project-system" // serviceAccountName created for the project const serviceAccountName = "project-controller-manager" // metricsServiceName is the name of the metrics service of the project const metricsServiceName = "project-controller-manager-metrics-service" // metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data const metricsRoleBindingName = "project-metrics-binding" var _ = Describe("Manager", Ordered, func() { var controllerPodName string // Before running the tests, set up the environment by creating the namespace, // enforce the restricted security policy to the namespace, installing CRDs, // and deploying the controller. BeforeAll(func() { By("creating manager namespace") cmd := exec.Command("kubectl", "create", "ns", namespace) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create namespace") By("labeling the namespace to enforce the restricted security policy") cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace, "pod-security.kubernetes.io/enforce=restricted") _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy") By("installing CRDs") cmd = exec.Command("make", "install") _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs") By("deploying the controller-manager") cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager") }) // After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs, // and deleting the namespace. AfterAll(func() { By("cleaning up the curl pod for metrics") cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace) _, _ = utils.Run(cmd) By("undeploying the controller-manager") cmd = exec.Command("make", "undeploy") _, _ = utils.Run(cmd) By("uninstalling CRDs") cmd = exec.Command("make", "uninstall") _, _ = utils.Run(cmd) By("removing manager namespace") cmd = exec.Command("kubectl", "delete", "ns", namespace) _, _ = utils.Run(cmd) }) // After each test, check for failures and collect logs, events, // and pod descriptions for debugging. AfterEach(func() { By("Cleaning up test CronJob resources") cmd := exec.Command("kubectl", "delete", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace, "--ignore-not-found=true") _, _ = utils.Run(cmd) cmd = exec.Command("kubectl", "delete", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace, "--ignore-not-found=true") _, _ = utils.Run(cmd) specReport := CurrentSpecReport() if specReport.Failed() { By("Fetching controller manager pod logs") cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) controllerLogs, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err) } By("Fetching Kubernetes events") cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp") eventsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err) } By("Fetching curl-metrics logs") cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) metricsOutput, err := utils.Run(cmd) if err == nil { _, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput) } else { _, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err) } By("Fetching controller manager pod description") cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace) podDescription, err := utils.Run(cmd) if err == nil { fmt.Println("Pod description:\n", podDescription) } else { fmt.Println("Failed to describe controller pod") } } }) SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) Context("Manager", func() { It("should run successfully", func() { By("validating that the controller-manager pod is running as expected") verifyControllerUp := func(g Gomega) { // Get the name of the controller-manager pod cmd := exec.Command("kubectl", "get", "pods", "-l", "control-plane=controller-manager", "-o", "go-template={{ range .items }}"+ "{{ if not .metadata.deletionTimestamp }}"+ "{{ .metadata.name }}"+ "{{ \"\\n\" }}{{ end }}{{ end }}", "-n", namespace, ) podOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information") podNames := utils.GetNonEmptyLines(podOutput) g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running") controllerPodName = podNames[0] g.Expect(controllerPodName).To(ContainSubstring("controller-manager")) // Validate the pod's status cmd = exec.Command("kubectl", "get", "pods", controllerPodName, "-o", "jsonpath={.status.phase}", "-n", namespace, ) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status") } Eventually(verifyControllerUp).Should(Succeed()) }) It("should ensure the metrics endpoint is serving metrics", func() { By("creating a ClusterRoleBinding for the service account to allow access to metrics") cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName, "--clusterrole=project-metrics-reader", fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName), ) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding") By("validating that the metrics service is available") cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Metrics service should exist") By("validating that the ServiceMonitor for Prometheus is applied in the namespace") cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "ServiceMonitor should exist") By("getting the service account token") token, err := serviceAccountToken() Expect(err).NotTo(HaveOccurred()) Expect(token).NotTo(BeEmpty()) By("ensuring the controller pod is ready") verifyControllerPodReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace, "-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("True"), "Controller pod not ready") } Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying that the controller manager is serving the metrics server") verifyMetricsServerStarted := func(g Gomega) { cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(ContainSubstring("Serving metrics server"), "Metrics server not yet started") } Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed()) By("waiting for the webhook service endpoints to be ready") verifyWebhookEndpointsReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "endpointslices.discovery.k8s.io", "-n", namespace, "-l", "kubernetes.io/service-name=project-webhook-service", "-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist") g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready") } Eventually(verifyWebhookEndpointsReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying the mutating webhook server is ready") verifyMutatingWebhookReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "mutatingwebhookconfigurations.admissionregistration.k8s.io", "project-mutating-webhook-configuration", "-o", "jsonpath={.webhooks[0].clientConfig.caBundle}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "MutatingWebhookConfiguration should exist") g.Expect(output).ShouldNot(BeEmpty(), "Mutating webhook CA bundle not yet injected") } Eventually(verifyMutatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed()) By("verifying the validating webhook server is ready") verifyValidatingWebhookReady := func(g Gomega) { cmd := exec.Command("kubectl", "get", "validatingwebhookconfigurations.admissionregistration.k8s.io", "project-validating-webhook-configuration", "-o", "jsonpath={.webhooks[0].clientConfig.caBundle}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "ValidatingWebhookConfiguration should exist") g.Expect(output).ShouldNot(BeEmpty(), "Validating webhook CA bundle not yet injected") } Eventually(verifyValidatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed()) By("waiting additional time for webhook server to stabilize") time.Sleep(5 * time.Second) // +kubebuilder:scaffold:e2e-metrics-webhooks-readiness By("creating the curl-metrics pod to access the metrics endpoint") cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never", "--namespace", namespace, "--image=curlimages/curl:latest", "--overrides", fmt.Sprintf(`{ "spec": { "containers": [{ "name": "curl", "image": "curlimages/curl:latest", "command": ["/bin/sh", "-c"], "args": [ "for i in $(seq 1 30); do curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics && exit 0 || sleep 2; done; exit 1" ], "securityContext": { "readOnlyRootFilesystem": true, "allowPrivilegeEscalation": false, "capabilities": { "drop": ["ALL"] }, "runAsNonRoot": true, "runAsUser": 1000, "seccompProfile": { "type": "RuntimeDefault" } } }], "serviceAccountName": "%s" } }`, token, metricsServiceName, namespace, serviceAccountName)) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod") By("waiting for the curl-metrics pod to complete.") verifyCurlUp := func(g Gomega) { cmd := exec.Command("kubectl", "get", "pods", "curl-metrics", "-o", "jsonpath={.status.phase}", "-n", namespace) output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status") } Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed()) By("getting the metrics by checking curl-metrics logs") verifyMetricsAvailable := func(g Gomega) { metricsOutput, err := getMetricsOutput() g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") g.Expect(metricsOutput).NotTo(BeEmpty()) g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK")) } Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed()) }) It("should provisioned cert-manager", func() { By("validating that cert-manager has the certificate Secret") verifyCertManager := func(g Gomega) { cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace) _, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) } Eventually(verifyCertManager).Should(Succeed()) }) It("should have CA injection for mutating webhooks", func() { By("checking CA injection for mutating webhooks") verifyCAInjection := func(g Gomega) { cmd := exec.Command("kubectl", "get", "mutatingwebhookconfigurations.admissionregistration.k8s.io", "project-mutating-webhook-configuration", "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") mwhOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(len(mwhOutput)).To(BeNumerically(">", 10)) } Eventually(verifyCAInjection).Should(Succeed()) }) It("should have CA injection for validating webhooks", func() { By("checking CA injection for validating webhooks") verifyCAInjection := func(g Gomega) { cmd := exec.Command("kubectl", "get", "validatingwebhookconfigurations.admissionregistration.k8s.io", "project-validating-webhook-configuration", "-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}") vwhOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) } Eventually(verifyCAInjection).Should(Succeed()) }) It("should have CA injection for CronJob conversion webhook", func() { By("checking CA injection for CronJob conversion webhook") verifyCAInjection := func(g Gomega) { cmd := exec.Command("kubectl", "get", "customresourcedefinitions.apiextensions.k8s.io", "cronjobs.batch.tutorial.kubebuilder.io", "-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}") vwhOutput, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred()) g.Expect(len(vwhOutput)).To(BeNumerically(">", 10)) } Eventually(verifyCAInjection).Should(Succeed()) }) // +kubebuilder:scaffold:e2e-webhooks-checks // TODO: Customize the e2e test suite with scenarios specific to your project. // Consider applying sample/CR(s) and check their status and/or verifying // the reconciliation by using the metrics, i.e.: // metricsOutput, err := getMetricsOutput() // Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") // Expect(metricsOutput).To(ContainSubstring( // fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`, // strings.ToLower(), // )) It("should successfully convert between v1 and v2 versions", func() { By("waiting for the webhook service to be ready") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "endpoints", "-n", namespace, "-l", "control-plane=controller-manager", "-o", "jsonpath={.items[0].subsets[0].addresses[0].ip}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get webhook service endpoints") g.Expect(strings.TrimSpace(output)).NotTo(BeEmpty(), "Webhook endpoint should have an IP") }, time.Minute, time.Second).Should(Succeed()) By("creating a v1 CronJob with a specific schedule") cmd := exec.Command("kubectl", "apply", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create v1 CronJob") By("waiting for the v1 CronJob to be created") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace) output, err := utils.Run(cmd) if err != nil { // Log controller logs on failure for debugging logCmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace, "--tail=50") logs, _ := utils.Run(logCmd) _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs when CronJob not found:\n%s\n", logs) } g.Expect(err).NotTo(HaveOccurred(), "v1 CronJob should exist, output: "+output) }, time.Minute, time.Second).Should(Succeed()) By("fetching the v1 CronJob and verifying the schedule format") cmd = exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule}") v1Schedule, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get v1 CronJob schedule") Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"), "v1 schedule should be in cron format") By("fetching the same CronJob as v2 and verifying the converted schedule") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule.minute}") v2Minute, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule") g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"), "v2 schedule.minute should be converted from v1 schedule") }, time.Minute, time.Second).Should(Succeed()) By("creating a v2 CronJob with structured schedule fields") cmd = exec.Command("kubectl", "apply", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create v2 CronJob") By("verifying the v2 CronJob has the correct structured schedule") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule.minute}") v2Minute, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule") g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"), "v2 CronJob should have minute field set") }, time.Minute, time.Second).Should(Succeed()) By("fetching the v2 CronJob as v1 and verifying schedule conversion") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule}") v1Schedule, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get converted v1 schedule") // When v2 only has minute field set, it converts to "*/1 * * * *" g.Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"), "v1 schedule should be converted from v2 structured schedule") }, time.Minute, time.Second).Should(Succeed()) }) }) }) // serviceAccountToken returns a token for the specified service account in the given namespace. // It uses the Kubernetes TokenRequest API to generate a token by directly sending a request // and parsing the resulting token from the API response. func serviceAccountToken() (string, error) { const tokenRequestRawString = `{ "apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest" }` // Temporary file to store the token request secretName := fmt.Sprintf("%s-token-request", serviceAccountName) tokenRequestFile := filepath.Join("/tmp", secretName) err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644)) if err != nil { return "", err } var out string verifyTokenCreation := func(g Gomega) { // Execute kubectl command to create the token cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf( "/api/v1/namespaces/%s/serviceaccounts/%s/token", namespace, serviceAccountName, ), "-f", tokenRequestFile) output, err := cmd.CombinedOutput() g.Expect(err).NotTo(HaveOccurred()) // Parse the JSON output to extract the token var token tokenRequest err = json.Unmarshal(output, &token) g.Expect(err).NotTo(HaveOccurred()) out = token.Status.Token } Eventually(verifyTokenCreation).Should(Succeed()) return out, err } // getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint. func getMetricsOutput() (string, error) { By("getting the curl-metrics logs") cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace) return utils.Run(cmd) } // tokenRequest is a simplified representation of the Kubernetes TokenRequest API response, // containing only the token field that we need to extract. type tokenRequest struct { Status struct { Token string `json:"token"` } `json:"status"` } ================================================ FILE: docs/book/src/multiversion-tutorial/testdata/project/test/utils/utils.go ================================================ /* Copyright 2026 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 utils import ( "bufio" "bytes" "fmt" "os" "os/exec" "strings" . "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck ) const ( certmanagerVersion = "v1.20.0" certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" defaultKindBinary = "kind" defaultKindCluster = "kind" prometheusOperatorVersion = "v0.89.0" prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + "releases/download/%s/bundle.yaml" ) func warnError(err error) { _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) } // Run executes the provided command within this context func Run(cmd *exec.Cmd) (string, error) { dir, _ := GetProjectDir() cmd.Dir = dir if err := os.Chdir(cmd.Dir); err != nil { _, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err) } cmd.Env = append(os.Environ(), "GO111MODULE=on") command := strings.Join(cmd.Args, " ") _, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command) output, err := cmd.CombinedOutput() if err != nil { return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err) } return string(output), nil } // UninstallCertManager uninstalls the cert manager func UninstallCertManager() { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } // Delete leftover leases in kube-system (not cleaned by default) kubeSystemLeases := []string{ "cert-manager-cainjector-leader-election", "cert-manager-controller", } for _, lease := range kubeSystemLeases { cmd = exec.Command("kubectl", "delete", "lease", lease, "-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0") if _, err := Run(cmd); err != nil { warnError(err) } } } // InstallCertManager installs the cert manager bundle. func InstallCertManager() error { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "apply", "-f", url) if _, err := Run(cmd); err != nil { return err } // Wait for cert-manager-webhook to be ready, which can take time if cert-manager // was re-installed after uninstalling on a cluster. cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", "--for", "condition=Available", "--namespace", "cert-manager", "--timeout", "5m", ) _, err := Run(cmd) return err } // IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed // by verifying the existence of key CRDs related to Cert Manager. func IsCertManagerCRDsInstalled() bool { // List of common Cert Manager CRDs certManagerCRDs := []string{ "certificates.cert-manager.io", "issuers.cert-manager.io", "clusterissuers.cert-manager.io", "certificaterequests.cert-manager.io", "orders.acme.cert-manager.io", "challenges.acme.cert-manager.io", } // Execute the kubectl command to get all CRDs cmd := exec.Command("kubectl", "get", "crds") output, err := Run(cmd) if err != nil { return false } // Check if any of the Cert Manager CRDs are present crdList := GetNonEmptyLines(output) for _, crd := range certManagerCRDs { for _, line := range crdList { if strings.Contains(line, crd) { return true } } } return false } // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. func InstallPrometheusOperator() error { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "create", "-f", url) _, err := Run(cmd) return err } // UninstallPrometheusOperator uninstalls the prometheus func UninstallPrometheusOperator() { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } } // IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed // by verifying the existence of key CRDs related to Prometheus. func IsPrometheusCRDsInstalled() bool { // List of common Prometheus CRDs prometheusCRDs := []string{ "prometheuses.monitoring.coreos.com", "prometheusrules.monitoring.coreos.com", "prometheusagents.monitoring.coreos.com", } cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name") output, err := Run(cmd) if err != nil { return false } crdList := GetNonEmptyLines(output) for _, crd := range prometheusCRDs { for _, line := range crdList { if strings.Contains(line, crd) { return true } } } return false } // LoadImageToKindClusterWithName loads a local docker image to the kind cluster func LoadImageToKindClusterWithName(name string) error { cluster := defaultKindCluster if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { cluster = v } kindOptions := []string{"load", "docker-image", name, "--name", cluster} kindBinary := defaultKindBinary if v, ok := os.LookupEnv("KIND"); ok { kindBinary = v } cmd := exec.Command(kindBinary, kindOptions...) _, err := Run(cmd) return err } // GetNonEmptyLines converts given command output string into individual objects // according to line breakers, and ignores the empty elements in it. func GetNonEmptyLines(output string) []string { var res []string elements := strings.SplitSeq(output, "\n") for element := range elements { if element != "" { res = append(res, element) } } return res } // GetProjectDir will return the directory where the project is func GetProjectDir() (string, error) { wd, err := os.Getwd() if err != nil { return wd, fmt.Errorf("failed to get current working directory: %w", err) } wd = strings.ReplaceAll(wd, "/test/e2e", "") return wd, nil } // UncommentCode searches for target in the file and remove the comment prefix // of the target content. The target content may span multiple lines. func UncommentCode(filename, target, prefix string) error { // false positive // nolint:gosec content, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } strContent := string(content) idx := strings.Index(strContent, target) if idx < 0 { return fmt.Errorf("unable to find the code %q to be uncommented", target) } out := new(bytes.Buffer) _, err = out.Write(content[:idx]) if err != nil { return fmt.Errorf("failed to write to output: %w", err) } scanner := bufio.NewScanner(bytes.NewBufferString(target)) if !scanner.Scan() { return nil } for { if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil { return fmt.Errorf("failed to write to output: %w", err) } // Avoid writing a newline in case the previous line was the last in target. if !scanner.Scan() { break } if _, err = out.WriteString("\n"); err != nil { return fmt.Errorf("failed to write to output: %w", err) } } if _, err = out.Write(content[idx+len(target):]); err != nil { return fmt.Errorf("failed to write to output: %w", err) } // false positive // nolint:gosec if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil { return fmt.Errorf("failed to write file %q: %w", filename, err) } return nil } ================================================ FILE: docs/book/src/multiversion-tutorial/tutorial.md ================================================ # Tutorial: Multi-Version API Most projects start out with an alpha API that changes release to release. However, eventually, most projects will need to move to a more stable API. Once your API is stable though, you can't make breaking changes to it. That's where API versions come into play. Let's make some changes to the `CronJob` API spec and make sure all the different versions are supported by our CronJob project. If you haven't already, make sure you've gone through the base [CronJob Tutorial](/cronjob-tutorial/cronjob-tutorial.md). Next, let's figure out what changes we want to make... ================================================ FILE: docs/book/src/multiversion-tutorial/webhooks.md ================================================ # Setting up the webhooks Our conversion is in place, so all that's left is to tell controller-runtime about our conversion. ## Webhook setup for v1... The v1 webhook handles conversion (as the hub) and provides validation/defaulting for the v1 CronJob format with a string-based schedule: {{#literatego ./testdata/project/internal/webhook/v1/cronjob_webhook.go}} ## Webhook setup for v2... The v2 webhook provides validation and defaulting for the v2 CronJob format with the structured CronSchedule type. Note how the validation logic differs from v1 - it builds a cron expression from the individual schedule fields: {{#literatego ./testdata/project/internal/webhook/v2/cronjob_webhook.go}} ## ...and `main.go` Similarly, our existing main file is sufficient: {{#literatego ./testdata/project/cmd/main.go}} Everything's set up and ready to go! All that's left now is to test out our webhooks. ================================================ FILE: docs/book/src/plugins/available/autoupdate-v1-alpha.md ================================================ # AutoUpdate (`autoupdate/v1-alpha`) Keeping your Kubebuilder project up to date with the latest improvements shouldn’t be a chore. With a small amount of setup, you can receive **automatic Pull Request** suggestions whenever a new Kubebuilder release is available — keeping your project **maintained, secure, and aligned with ecosystem changes**. This automation uses the [`kubebuilder alpha update`][alpha-update-command] command with a **3-way merge strategy** to refresh your project scaffold, and wraps it in a GitHub Actions workflow that opens an **Issue** with a **Pull Request compare link** so you can create the PR and review it. ## When to Use It - When you want to reduce the burden of keeping the project updated and well-maintained. - When you want guidance and help from AI to know what changes are needed to keep your project up to date and to solve conflicts (requires `--use-gh-models` flag and GitHub Models permissions). ## How to Use It - If you want to add the `autoupdate` plugin to your project: ```shell kubebuilder edit --plugins="autoupdate/v1-alpha" ``` - If you want to create a new project with the `autoupdate` plugin: ```shell kubebuilder init --plugins=go/v4,autoupdate/v1-alpha ``` ### Optional: GitHub Models AI Summary By default, the workflow works without GitHub Models to avoid permission errors. If you want AI-generated summaries in your update issues: ```shell kubebuilder edit --plugins="autoupdate/v1-alpha" --use-gh-models ``` ## How It Works The plugin scaffolds a GitHub Actions workflow that checks for new Kubebuilder releases every week. When an update is available, it: 1. Creates a new branch with the merged changes 2. Opens a GitHub Issue with a PR compare link **Example Issue:** Example Issue **With GitHub Models enabled** (optional), you also get AI-generated summaries: AI Summary **Conflict help** (when needed): Conflicts ## Customizing the Workflow The generated workflow uses the `kubebuilder alpha update` command with default flags. You can customize the workflow by editing `.github/workflows/auto_update.yml` to add additional flags: **Default flags used:** - `--force` - Continue even if conflicts occur (automation-friendly) - `--push` - Automatically push the output branch to remote - `--restore-path .github/workflows` - Preserve CI workflows from base branch - `--open-gh-issue` - Create a GitHub Issue with PR compare link - `--use-gh-models` - (optional) Add AI summary to the issue **Additional available flags:** - `--merge-message` - Custom commit message for clean merges - `--conflict-message` - Custom commit message when conflicts occur - `--from-version` - Specify the version to upgrade from - `--to-version` - Specify the version to upgrade to - `--output-branch` - Custom output branch name - `--show-commits` - Keep full history instead of squashing - `--git-config` - Pass per-invocation Git config For complete documentation on all available flags, see the [`kubebuilder alpha update`][alpha-update-command] reference. **Example: Customize commit messages** Edit `.github/workflows/auto_update.yml`: ```yaml - name: Run kubebuilder alpha update run: | kubebuilder alpha update \ --force \ --push \ --restore-path .github/workflows \ --open-gh-issue \ --merge-message "chore: update kubebuilder scaffold" \ --conflict-message "chore: update with conflicts - review needed" ``` ## Troubleshooting #### If you get the 403 Forbidden Error **Error message:** ``` ERROR Update failed error=failed to open GitHub issue: gh models run failed: exit status 1 Error: unexpected response from the server: 403 Forbidden ``` **Quick fix:** Disable GitHub Models (works for everyone) ```shell kubebuilder edit --plugins="autoupdate/v1-alpha" ``` This regenerates the workflow without GitHub Models: ```yaml permissions: contents: write issues: write # No models: read permission steps: - name: Checkout repository uses: actions/checkout@v4 # ... other setup steps - name: Run kubebuilder alpha update # WARNING: This workflow does not use GitHub Models AI summary by default. # To enable AI-generated summaries, you need permissions to use GitHub Models. # If you have the required permissions, re-run: # kubebuilder edit --plugins="autoupdate/v1-alpha" --use-gh-models run: | kubebuilder alpha update \ --force \ --push \ --restore-path .github/workflows \ --open-gh-issue ``` The workflow continues to work—just without AI summaries. **To enable GitHub Models instead:** 1. Ask your GitHub administrator to enable Models (see links below) 2. Enable it in **Settings → Code and automation → Models** 3. Re-run with: ```shell kubebuilder edit --plugins="autoupdate/v1-alpha" --use-gh-models ``` This regenerates the workflow WITH GitHub Models: ```yaml permissions: contents: write issues: write models: read # Added for GitHub Models steps: - name: Checkout repository uses: actions/checkout@v4 # ... other setup steps - name: Install gh-models extension run: | gh extension install github/gh-models --force gh models --help >/dev/null - name: Run kubebuilder alpha update # --use-gh-models: Adds an AI-generated comment to the Issue with # a summary of scaffold changes and conflict-resolution guidance (if any). run: | kubebuilder alpha update \ --force \ --push \ --restore-path .github/workflows \ --open-gh-issue \ --use-gh-models ``` ## Demonstration [alpha-update-command]: ./../../reference/commands/alpha_update.md [ai-models]: https://docs.github.com/en/github-models/about-github-models [manage-models-at-scale]: https://docs.github.com/en/github-models/github-models-at-scale/manage-models-at-scale [manage-org-models]: https://docs.github.com/en/organizations/managing-organization-settings/managing-or-restricting-github-models-for-your-organization ================================================ FILE: docs/book/src/plugins/available/deploy-image-plugin-v1-alpha.md ================================================ # Deploy Image Plugin (deploy-image/v1-alpha) The `deploy-image` plugin allows users to create [controllers][controller-runtime] and custom resources that deploy and manage container images on the cluster, following Kubernetes best practices. It simplifies the complexities of deploying images while allowing users to customize their projects as needed. By using this plugin, you will get: - A controller implementation to deploy and manage an Operand (image) on the cluster. - Tests to verify the reconciliation logic, using [ENVTEST][envtest]. - Custom resource samples updated with the necessary specifications. - Environment variable support for managing the Operand (image) within the manager. ## When to use it? - This plugin is ideal for users who are just getting started with Kubernetes operators. - It helps users deploy and manage an image (Operand) using the [Operator pattern][operator-pattern]. - If you're looking for a quick and efficient way to set up a custom controller and manage a container image, this plugin is a great choice. ## How to use it? 1. **Initialize your project**: After creating a new project with `kubebuilder init`, you can use this plugin to create APIs. Ensure that you've completed the [quick start][quick-start] guide before proceeding. 2. **Create APIs**: With this plugin, you can [create APIs][create-apis] to specify the image (Operand) you want to deploy on the cluster. You can also optionally specify the command, port, and security context using various flags: Example command: ```sh kubebuilder create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:1.6.15-alpine --image-container-command="memcached,--memory-limit=64,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha" ``` ## Subcommands The `deploy-image` plugin includes the following subcommand: - `create api`: Use this command to scaffold the API and controller code to manage the container image. ## Affected files When using the `create api` command with this plugin, the following files are affected, in addition to the existing Kubebuilder scaffolding: - `controllers/*_controller_test.go`: Scaffolds tests for the controller. - `controllers/*_suite_test.go`: Scaffolds or updates the test suite. - `api//*_types.go`: Scaffolds the API specs. - `config/samples/*_.yaml`: Scaffolds default values for the custom resource. - `main.go`: Updates the file to add the controller setup. - `config/manager/manager.yaml`: Updates to include environment variables for storing the image. ## Further Resources: - Check out this [video][video] to see how it works. [video]: https://youtu.be/UwPuRjjnMjY [operator-pattern]: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [testdata]: https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v4-with-plugins [envtest]: ./../../reference/envtest.md [quick-start]: ./../../quick-start.md [create-apis]: ../../cronjob-tutorial/new-api.md ================================================ FILE: docs/book/src/plugins/available/go-v4-plugin.md ================================================ # go/v4 (go.kubebuilder.io/v4) **(Default Scaffold)** Kubebuilder will scaffold using the `go/v4` plugin only if specified when initializing the project. This plugin is a composition of the `kustomize.common.kubebuilder.io/v2` and `base.go.kubebuilder.io/v4` plugins using the [Bundle Plugin][bundle]. It scaffolds a project template that helps in constructing sets of [controllers][controller-runtime]. By following the [quickstart][quickstart] and creating any project, you will be using this plugin by default. ## How to use it ? To create a new project with the `go/v4` plugin the following command can be used: ```sh kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/project --plugins=go/v4 ``` ## Subcommands supported by the plugin - Init - `kubebuilder init [OPTIONS]` - Edit - `kubebuilder edit [OPTIONS]` - Create API - `kubebuilder create api [OPTIONS]` - Create Webhook - `kubebuilder create webhook [OPTIONS]` ## Further resources - To see the composition of plugins, you can check the source code for the Kubebuilder [main.go][plugins-main]. - Check the code implementation of the [base Golang plugin `base.go.kubebuilder.io/v4`][v4-plugin]. - Check the code implementation of the [Kustomize/v2 plugin][kustomize-plugin]. - Check [controller-runtime][controller-runtime] to know more about controllers. [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [quickstart]: ./../../quick-start.md [testdata]: https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata [plugins-main]: ./../../../../../cmd/main.go [kustomize-plugin]: ./../../plugins/available/kustomize-v2.md [kustomize]: https://github.com/kubernetes-sigs/kustomize [standard-go-project]: https://github.com/golang-standards/project-layout [v4-plugin]: ./../../../../../pkg/plugins/golang/v4 [migration-guide-doc]: ./../../migration/migration_guide_gov3_to_gov4.md [project-doc]: ./../../reference/project-config.md [bundle]: ./../../../../../pkg/plugin/bundle.go ================================================ FILE: docs/book/src/plugins/available/grafana-v1-alpha.md ================================================ # Grafana Plugin (`grafana/v1-alpha`) The Grafana plugin is an optional plugin that can be used to scaffold Grafana Dashboards to allow you to check out the default metrics which are exported by projects using [controller-runtime][controller-runtime]. ## When to use it ? - If you are looking to observe the metrics exported by [controller metrics][controller-metrics] and collected by Prometheus via [Grafana][grafana]. ## How to use it ? ### Prerequisites: - Your project must be using [controller-runtime][controller-runtime] to expose the metrics via the [controller default metrics][controller-metrics] and they need to be collected by Prometheus. - Access to [Prometheus][prometheus]. - Prometheus should have an endpoint exposed. (For `prometheus-operator`, this is similar as: http://prometheus-k8s.monitoring.svc:9090 ) - The endpoint is ready to/already become the datasource of your Grafana. See [Add a data source](https://grafana.com/docs/grafana/latest/datasources/add-a-data-source/) - Access to [Grafana][grafana-install]. Make sure you have: - [Dashboard edit permission][grafana-permissions] - Prometheus Data source ![pre][prometheus-data-source] ### Basic Usage The Grafana plugin is attached to the `init` subcommand and the `edit` subcommand: ```sh # Initialize a new project with grafana plugin kubebuilder init --plugins grafana.kubebuilder.io/v1-alpha # Enable grafana plugin to an existing project kubebuilder edit --plugins grafana.kubebuilder.io/v1-alpha ``` The plugin will create a new directory and scaffold the JSON files under it (i.e. `grafana/controller-runtime-metrics.json`). #### Show case: See an example of how to use the plugin in your project: ![output](https://user-images.githubusercontent.com/18136486/175382307-9a6c3b8b-6cc7-4339-b221-2539d0fec042.gif) #### Now, let's check how to use the Grafana dashboards 1. Copy the JSON file 2. Visit `/dashboard/import` to [import a new dashboard](https://grafana.com/docs/grafana/latest/dashboards/export-import/#import-dashboard). 3. Paste the JSON content to `Import via panel json`, then press `Load` button 4. Select the data source for Prometheus metrics 5. Once the json is imported in Grafana, the dashboard is ready. ### Grafana Dashboard #### Controller Runtime Reconciliation total & errors - Metrics: - controller_runtime_reconcile_total - controller_runtime_reconcile_errors_total - Query: - sum(rate(controller_runtime_reconcile_total{job="$job"}[5m])) by (instance, pod) - sum(rate(controller_runtime_reconcile_errors_total{job="$job"}[5m])) by (instance, pod) - Description: - Per-second rate of total reconciliation as measured over the last 5 minutes - Per-second rate of reconciliation errors as measured over the last 5 minutes - Sample: #### Controller CPU & Memory Usage - Metrics: - process_cpu_seconds_total - process_resident_memory_bytes - Query: - rate(process_cpu_seconds_total{job="$job", namespace="$namespace", pod="$pod"}[5m]) \* 100 - process_resident_memory_bytes{job="$job", namespace="$namespace", pod="$pod"} - Description: - Per-second rate of CPU usage as measured over the last 5 minutes - Allocated Memory for the running controller - Sample: #### Seconds of P50/90/99 Items Stay in Work Queue - Metrics - workqueue_queue_duration_seconds_bucket - Query: - histogram_quantile(0.50, sum(rate(workqueue_queue_duration_seconds_bucket{job="$job", namespace="$namespace"}[5m])) by (instance, name, le)) - Description - Seconds an item stays in workqueue before being requested. - Sample: #### Seconds of P50/90/99 Items Processed in Work Queue - Metrics - workqueue_work_duration_seconds_bucket - Query: - histogram_quantile(0.50, sum(rate(workqueue_work_duration_seconds_bucket{job="$job", namespace="$namespace"}[5m])) by (instance, name, le)) - Description - Seconds of processing an item from workqueue takes. - Sample: #### Add Rate in Work Queue - Metrics - workqueue_adds_total - Query: - sum(rate(workqueue_adds_total{job="$job", namespace="$namespace"}[5m])) by (instance, name) - Description - Per-second rate of items added to work queue - Sample: #### Retries Rate in Work Queue - Metrics - workqueue_retries_total - Query: - sum(rate(workqueue_retries_total{job="$job", namespace="$namespace"}[5m])) by (instance, name) - Description - Per-second rate of retries handled by workqueue - Sample: #### Number of Workers in Use - Metrics - controller_runtime_active_workers - Query: - controller_runtime_active_workers{job="$job", namespace="$namespace"} - Description - The number of active controller workers - Sample: #### WorkQueue Depth - Metrics - workqueue_depth - Query: - workqueue_depth{job="$job", namespace="$namespace"} - Description - Current depth of workqueue - Sample: #### Unfinished Seconds - Metrics - workqueue_unfinished_work_seconds - Query: - rate(workqueue_unfinished_work_seconds{job="$job", namespace="$namespace"}[5m]) - Description - How many seconds of work has done that is in progress and hasn't been observed by work_duration. - Sample: ### Visualize Custom Metrics The Grafana plugin supports scaffolding manifests for custom metrics. #### Generate Config Template When the plugin is triggered for the first time, `grafana/custom-metrics/config.yaml` is generated. ```yaml --- customMetrics: # - metric: # Raw custom metric (required) # type: # Metric type: counter/gauge/histogram (required) # expr: # Prom_ql for the metric (optional) # unit: # Unit of measurement, examples: s,none,bytes,percent,etc. (optional) ``` #### Add Custom Metrics to Config You can enter multiple custom metrics in the file. For each element, you need to specify the `metric` and its `type`. The Grafana plugin can automatically generate `expr` for visualization. Alternatively, you can provide `expr` and the plugin will use the specified one directly. ```yaml --- customMetrics: - metric: memcached_operator_reconcile_total # Raw custom metric (required) type: counter # Metric type: counter/gauge/histogram (required) unit: none - metric: memcached_operator_reconcile_time_seconds_bucket type: histogram ``` #### Scaffold Manifest Once `config.yaml` is configured, you can run `kubebuilder edit --plugins grafana.kubebuilder.io/v1-alpha` again. This time, the plugin will generate `grafana/custom-metrics/custom-metrics-dashboard.json`, which can be imported to Grafana UI. #### Show case: See an example of how to visualize your custom metrics: ![output2][show-case] ## Subcommands The Grafana plugin implements the following subcommands: - edit (`$ kubebuilder edit [OPTIONS]`) - init (`$ kubebuilder init [OPTIONS]`) ## Affected files The following scaffolds will be created or updated by this plugin: - `grafana/*.json` ## Further resources - Check out [video to show how it works][video] - Checkout the [video to show how the custom metrics feature works][video-custom-metrics] - Refer to a sample of `serviceMonitor` provided by [kustomize plugin][kustomize-plugin] - Check the [plugin implementation][plugin-implementation] - [Grafana Docs][grafana-docs] of importing JSON file - The usage of serviceMonitor by [Prometheus Operator][servicemonitor] [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [grafana]: https://grafana.com/docs/grafana/next/ [grafana-docs]: https://grafana.com/docs/grafana/latest/dashboards/export-import/#import-dashboard [kube-prometheus]: https://github.com/prometheus-operator/kube-prometheus [prometheus]: https://prometheus.io/docs/introduction/overview/ [prom-operator]: https://prometheus-operator.dev/docs/prologue/introduction/ [servicemonitor]: https://github.com/prometheus-operator/prometheus-operator/blob/main/Documentation/user-guides/getting-started.md#related-resources [grafana-install]: https://grafana.com/docs/grafana/latest/setup-grafana/installation/ [grafana-permissions]: https://grafana.com/docs/grafana/next/administration/roles-and-permissions/#dashboard-permissions [prometheus-data-source]: https://user-images.githubusercontent.com/18136486/176119794-f6d69b0b-93f0-4f9e-a53c-daf9f77dadae.gif [video]: https://youtu.be/-w_JjcV8jXc [video-custom-metrics]: https://youtu.be/x_0FHta2HXc [show-case]: https://user-images.githubusercontent.com/18136486/186933170-d2e0de71-e079-4d1b-906a-99a549d66ebf.gif [controller-metrics]: ./../../reference/metrics-reference.md [kustomize-plugin]: ./../../../../../testdata/project-v4-with-plugins/config/prometheus/monitor.yaml [plugin-implementation]: ./../../../../../pkg/plugins/optional/grafana/ [reference-metrics-doc]: ./../../reference/metrics.md#exporting-metrics-for-prometheus [testdata]: https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v4-with-plugins ================================================ FILE: docs/book/src/plugins/available/helm-v1-alpha.md ================================================ # Helm Plugin (`helm/v1-alpha`) - **DEPRECATED** The Helm plugin is an optional plugin that can be used to scaffold a Helm chart, allowing you to distribute the project using Helm. By default, users can generate a bundle with all the manifests by running the following command: ```bash make build-installer IMG=/ ``` This allows the project consumer to install the solution by applying the bundle with: ```bash kubectl apply -f https://raw.githubusercontent.com//project-v4//dist/install.yaml ``` However, in many scenarios, you might prefer to provide a Helm chart to package your solution. If so, you can use this plugin to generate the Helm chart under the `dist` directory. ## When to use it - If you want to provide a Helm chart for users to install and manage your project. - If you need to update the Helm chart generated under `dist/chart/` with the latest project changes: - After generating new manifests, use the `edit` option to sync the Helm chart. - **IMPORTANT:** If you have created a webhook or an API using the [DeployImage][deployImage-plugin] plugin, you must run the `edit` command with the `--force` flag to regenerate the Helm chart values based on the latest manifests (_after running `make manifests`_) to ensure that the HelmChart values are updated accordingly. In this case, if you have customized the files under `dist/chart/values.yaml`, and the `templates/manager/manager.yaml`, you will need to manually reapply your customizations on top of the latest changes after regenerating the Helm chart. ## How to use it ? ### Basic Usage The Helm plugin is attached to the `edit` subcommand as the `helm/v1-alpha` plugin relies on the Go project being scaffolded first. ```sh # Initialize a new project kubebuilder init # Enable or Update the helm chart via the helm plugin to an existing project # Before run the edit command, run `make manifests` to generate the manifest under `config/` make manifests kubebuilder edit --plugins=helm/v1-alpha ``` ## Subcommands The Helm plugin implements the following subcommands: - edit (`$ kubebuilder edit [OPTIONS]`) ## Affected files The following scaffolds will be created or updated by this plugin: - `dist/chart/*` [testdata]: https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v4-with-plugins [deployImage-plugin]: ./deploy-image-plugin-v1-alpha.md ================================================ FILE: docs/book/src/plugins/available/helm-v2-alpha.md ================================================ # Helm Plugin `(helm/v2-alpha)` The Helm plugin **v2-alpha** provides a way to package your project as a Helm chart, enabling distribution in Helm’s native format. Instead of using static templates, this plugin dynamically generates Helm charts from your project’s **kustomize output** (via `make build-installer`). It keeps your custom settings such as environment variables, labels, annotations, and security contexts. This lets you deliver your Kubebuilder project in two ways: - As a **bundle** (`dist/install.yaml`) generated with kustomize - As a **Helm chart** that matches the same output ## Why Helm? By default, you can create a bundle of manifests with: ```shell make build-installer IMG=/ ``` Users can install it directly: ```shell kubectl apply -f https://raw.githubusercontent.com//project-v4//dist/install.yaml ``` But many people prefer Helm for packaging, upgrades, and distribution. The **helm/v2-alpha** plugin converts the bundle (`dist/install.yaml`) into a Helm chart that mirrors your project. ## Key Features - **Dynamic Generation**: Charts are built from real kustomize output, not boilerplate. - **Preserves Customizations**: Keeps env vars, labels, annotations, and patches. - **Structured Output**: Templates follow your `config/` directory layout. - **Smart Values**: `values.yaml` includes only actual configurable parameters. - **File Preservation**: `Chart.yaml` is never overwritten. Without `--force`, `values.yaml`, `NOTES.txt`, `_helpers.tpl`, `.helmignore` and `.github/workflows/test-chart.yml` are preserved. - **Handles Custom Resources**: Resources not matching standard layout (custom Services, ConfigMaps, etc.) are placed in `templates/extras/` with proper templating. ## When to Use It Use the **helm/v2-alpha** plugin if: - You want Helm charts that stay true to your kustomize setup - You need charts that update with your project automatically - You want a clean template layout similar to `config/` - You want to distribute your solution using either this format ## Usage ### Basic Workflow ```shell # Create a new project kubebuilder init # Build the installer bundle make build-installer IMG=/ # Create Helm chart from kustomize output kubebuilder edit --plugins=helm/v2-alpha # Regenerate preserved files (Chart.yaml never overwritten) kubebuilder edit --plugins=helm/v2-alpha --force ``` ### Advanced Options ```shell # Use a custom manifests file kubebuilder edit --plugins=helm/v2-alpha --manifests=manifests/custom-install.yaml # Write chart to a custom output directory kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts # Combine manifests and output kubebuilder edit --plugins=helm/v2-alpha \ --manifests=manifests/install.yaml \ --output-dir=helm-charts ``` ## Chart Structure The plugin creates a chart layout that matches your `config/`: ```shell /chart/ ├── Chart.yaml ├── values.yaml ├── .helmignore └── templates/ ├── NOTES.txt ├── _helpers.tpl ├── rbac/ # Individual RBAC files (examples) │ ├── controller-manager.yaml │ ├── leader-election-role.yaml │ ├── leader-election-rolebinding.yaml │ ├── manager-role.yaml │ ├── manager-rolebinding.yaml │ ├── metrics-auth-role.yaml │ ├── metrics-auth-rolebinding.yaml │ ├── metrics-reader.yaml │ ├── memcached-admin-role.yaml │ ├── memcached-editor-role.yaml │ ├── memcached-viewer-role.yaml │ ├── busybox-admin-role.yaml │ ├── busybox-editor-role.yaml │ ├── busybox-viewer-role.yaml │ └── ... ├── crd/ # Individual CRD files (examples) │ ├── busyboxes.example.com.testproject.org.yaml │ └── ... ├── cert-manager/ │ ├── metrics-certs.yaml │ ├── selfsigned-issuer.yaml │ └── serving-cert.yaml ├── manager/ │ └── manager.yaml ├── metrics/ │ └── controller-manager-metrics-service.yaml ├── webhook/ │ ├── validating-webhook-configuration.yaml │ └── webhook-service.yaml ├── monitoring/ │ └── servicemonitor.yaml └── extras/ # Custom resources (if any) ├── my-service.yaml └── my-config.yaml ``` ## Post-Install Notes The plugin generates a `NOTES.txt` template that displays helpful information after `helm install` or `helm upgrade`: - Installation confirmation with release name and namespace - Commands to verify the deployment (kubectl get pods, CRDs) - How to get more information using helm commands The `NOTES.txt` file is preserved on subsequent runs (unless `--force` is used), allowing you to customize the post-install message for your users. ## Values Configuration The generated `values.yaml` provides configuration options extracted from your actual deployment. Namespace creation is not managed by the chart; use Helm's `--namespace` and `--create-namespace` flags when installing. **Example** ```yaml ## String to partially override chart.fullname template (will maintain the release name) ## # nameOverride: "" ## String to fully override chart.fullname template ## # fullnameOverride: "" ## Configure the controller manager deployment ## manager: replicas: 1 image: repository: controller tag: latest pullPolicy: IfNotPresent ## Arguments ## args: - --leader-elect ## Environment variables ## env: - name: BUSYBOX_IMAGE value: busybox:1.36.1 - name: MEMCACHED_IMAGE value: memcached:1.6.26-alpine3.19 ## Image pull secrets ## imagePullSecrets: [] # Example: # imagePullSecrets: # - name: myregistrykey ## Pod-level security settings ## podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault ## Container-level security settings ## securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: true ## Resource limits and requests ## resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi ## Manager pod's affinity ## affinity: {} # Example: # affinity: # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: kubernetes.io/arch # operator: In # values: # - amd64 # - arm64 ## Manager pod's node selector ## nodeSelector: {} # Example: # nodeSelector: # kubernetes.io/os: linux # disktype: ssd ## Manager pod's tolerations ## tolerations: [] # Example: # tolerations: # - key: "node.kubernetes.io/unreachable" # operator: "Exists" # effect: "NoExecute" # tolerationSeconds: 6000 ## Helper RBAC roles for managing custom resources ## rbacHelpers: # Install convenience admin/editor/viewer roles for CRDs enable: false ## Custom Resource Definitions ## crd: # Install CRDs with the chart enable: true # Keep CRDs when uninstalling keep: true ## Controller metrics endpoint. ## Enable to expose /metrics endpoint with RBAC protection. ## metrics: enable: true # Metrics server port port: 8443 ## Cert-manager integration for TLS certificates. ## Required for webhook certificates and metrics endpoint certificates. ## certManager: enable: true ## Webhook server configuration ## webhook: enable: true # Webhook server port port: 9443 ## Prometheus ServiceMonitor for metrics scraping. ## Requires prometheus-operator to be installed in the cluster. ## prometheus: enable: false ``` ### Installation The first time you run the plugin, it adds convenient Helm deployment targets to your `Makefile`: ```shell make helm-deploy IMG=/ # Deploy/upgrade the chart make helm-status # Check release status make helm-history # View release history make helm-rollback # Rollback to previous version make helm-uninstall # Remove the release ``` You can also install manually using Helm commands: ```shell helm install my-release ./dist/chart \ --namespace my-project-system \ --create-namespace ``` The Makefile targets use sensible defaults extracted from your project configuration (namespace from manifests, release name from project name, chart directory from `--output-dir` flag). ## Flags | Flag | Description | |---------------------|-----------------------------------------------------------------------------| | **--manifests** | Path to YAML file containing Kubernetes manifests (default: `dist/install.yaml`) | | **--output-dir** string | Output directory for chart (default: `dist`) | | **--force** | Regenerates preserved files except `Chart.yaml` (`values.yaml`, `NOTES.txt`, `_helpers.tpl`, `.helmignore`, `test-chart.yml`) | ================================================ FILE: docs/book/src/plugins/available/kustomize-v2.md ================================================ # Kustomize v2 **(Default Scaffold)** The Kustomize plugin allows you to scaffold all kustomize manifests used with the language base plugin `base.go.kubebuilder.io/v4`. This plugin is used to generate the manifest under the `config/` directory for projects built within the `go/v4` plugin (default scaffold). Projects like [Operator-sdk][sdk] use the Kubebuilder project as a library and provide options for working with other languages such as Ansible and Helm. The Kustomize plugin helps them maintain consistent configuration across languages. It also simplifies the creation of plugins that perform changes on top of the default scaffold, removing the need for manual updates across multiple language plugins. This approach allows the creation of "helper" plugins that work with different projects and languages. ## How to use it If you want your language plugin to use kustomize, use the [Bundle Plugin][bundle] to specify that your language plugin is composed of your language-specific plugin and kustomize for its configuration, as shown: ```go import ( ... kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2" golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4" ... ) // Bundle plugin for Golang projects scaffolded by Kubebuilder go/v4 gov4Bundle, _ := plugin.NewBundle(plugin.WithName(golang.DefaultNameQualifier), plugin.WithVersion(plugin.Version{Number: 4}), plugin.WithPlugins(kustomizecommonv2.Plugin{}, golangv4.Plugin{}), // Scaffold the config/ directory and all kustomize files ) ``` You can also use kustomize/v2 alone via: ```sh kubebuilder init --plugins=kustomize/v2 $ ls -la total 24 drwxr-xr-x 6 camilamacedo86 staff 192 31 Mar 09:56 . drwxr-xr-x 11 camilamacedo86 staff 352 29 Mar 21:23 .. -rw------- 1 camilamacedo86 staff 129 26 Mar 12:01 .dockerignore -rw------- 1 camilamacedo86 staff 367 26 Mar 12:01 .gitignore -rw------- 1 camilamacedo86 staff 94 31 Mar 09:56 PROJECT drwx------ 6 camilamacedo86 staff 192 31 Mar 09:56 config ``` Or combined with the base language plugins: ```sh # Provides the same scaffold of go/v4 plugin which is composition but with kustomize/v2 kubebuilder init --plugins=kustomize/v2,base.go.kubebuilder.io/v4 --domain example.org --repo example.org/guestbook-operator ``` ## Subcommands The kustomize plugin implements the following subcommands: * init (`$ kubebuilder init [OPTIONS]`) * create api (`$ kubebuilder create api [OPTIONS]`) * create webhook (`$ kubebuilder create api [OPTIONS]`) ## Affected files The following scaffolds will be created or updated by this plugin: * `config/*` ## Further resources * Check the kustomize [plugin implementation](https://github.com/kubernetes-sigs/kubebuilder/tree/master/pkg/plugins/common/kustomize) * Check the [kustomize documentation][kustomize-docs] * Check the [kustomize repository][kustomize-github] [sdk]:https://github.com/operator-framework/operator-sdk [kustomize-docs]: https://kustomize.io/ [kustomize-github]: https://github.com/kubernetes-sigs/kustomize [kustomize-replacements]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/replacements/ [kustomize-vars]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/vars/ [release-notes-v5]: https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.0.0 [release-notes-v4]: https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv4.0.0 [testdata]: ./../../../../../testdata/ [bundle]: ./../../../../../pkg/plugin/bundle.go [kustomize-create-api]: ./../../../../../pkg/plugins/common/kustomize/v2/scaffolds/api.go ================================================ FILE: docs/book/src/plugins/available-plugins.md ================================================ # Available plugins This section describes the plugins supported and shipped in with the Kubebuilder project. {{#include to-scaffold-project.md }} {{#include to-add-optional-features.md }} {{#include to-be-extended.md }} [plugin-versions]: plugins-versioning.md ================================================ FILE: docs/book/src/plugins/extending/custom-markers.md ================================================ # Creating Custom Markers ## Overview When using Kubebuilder as a library, you may need to scaffold files with extensions that aren't natively supported by Kubebuilder's marker system. This guide shows you how to create custom marker support for any file extension. ## When to Use Custom Markers Custom markers are useful when: - You're building an external plugin for languages not natively supported by Kubebuilder - You want to scaffold files with custom extensions (`.rs`, `.java`, `.py`, `.tpl`, etc.) - You need scaffolding markers in non-Go files for your own use cases - Your file extensions aren't (and shouldn't be) part of the core `commentsByExt` map ## Understanding Markers Markers are special comments used by Kubebuilder for scaffolding purposes. They indicate where code can be inserted or modified. The core Kubebuilder marker system only supports `.go`, `.yaml`, and `.yml` files by default. Example of a marker in a Go file: ```go // +kubebuilder:scaffold:imports ``` ## Implementation Example Here's how to implement custom markers for Rust files (`.rs`). This same pattern can be applied to any file extension. ### Define Your Marker Type ```go // pkg/markers/rust.go package markers import ( "fmt" "path/filepath" "strings" ) const RustPluginPrefix = "+rust:scaffold:" type RustMarker struct { prefix string comment string value string } func NewRustMarker(path string, value string) (RustMarker, error) { ext := filepath.Ext(path) if ext != ".rs" { return RustMarker{}, fmt.Errorf("expected .rs file, got %s", ext) } return RustMarker{ prefix: formatPrefix(RustPluginPrefix), comment: "//", value: value, }, nil } func (m RustMarker) String() string { return m.comment + " " + m.prefix + m.value } func formatPrefix(prefix string) string { trimmed := strings.TrimSpace(prefix) var builder strings.Builder if !strings.HasPrefix(trimmed, "+") { builder.WriteString("+") } builder.WriteString(trimmed) if !strings.HasSuffix(trimmed, ":") { builder.WriteString(":") } return builder.String() } ``` ### Use in Template Generation ```go package templates import ( "fmt" "github.com/yourorg/yourplugin/pkg/markers" ) func GenerateRustFile(projectName string) (string, error) { marker, err := markers.NewRustMarker("src/main.rs", "imports") if err != nil { return "", err } content := fmt.Sprintf(`// Generated by Rust Plugin %s use std::error::Error; fn main() -> Result<(), Box> { println!("Hello from %s!"); Ok(()) } `, marker.String(), projectName) return content, nil } func GenerateCargoToml(projectName string) string { return fmt.Sprintf(`[package] name = "%s" version = "0.1.0" edition = "2021" [dependencies] `, projectName) } ``` ### Integrate with External Plugin ```go package main import ( "bufio" "encoding/json" "fmt" "io" "os" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" "github.com/yourorg/yourplugin/pkg/markers" ) func main() { // External plugins communicate via JSON over STDIN/STDOUT reader := bufio.NewReader(os.Stdin) input, err := io.ReadAll(reader) if err != nil { returnError(fmt.Errorf("error reading STDIN: %w", err)) return } pluginRequest := &external.PluginRequest{} err = json.Unmarshal(input, pluginRequest) if err != nil { returnError(fmt.Errorf("error unmarshaling request: %w", err)) return } var response external.PluginResponse switch pluginRequest.Command { case "init": response = handleInit(pluginRequest) default: response = external.PluginResponse{ Command: pluginRequest.Command, Error: true, ErrorMsgs: []string{fmt.Sprintf("unknown command: %s", pluginRequest.Command)}, } } output, err := json.Marshal(response) if err != nil { fmt.Fprintf(os.Stderr, "failed to marshal response: %v\n", err) os.Exit(1) } fmt.Printf("%s", output) } func handleInit(req *external.PluginRequest) external.PluginResponse { // Create Rust file with custom markers marker, err := markers.NewRustMarker("src/main.rs", "imports") if err != nil { return external.PluginResponse{ Command: "init", Error: true, ErrorMsgs: []string{fmt.Sprintf("failed to create Rust marker: %v", err)}, } } fileContent := fmt.Sprintf(`// Generated by Rust Plugin %s use std::error::Error; fn main() -> Result<(), Box> { println!("Hello from Rust!"); Ok(()) } `, marker.String()) // External plugins use "universe" to represent file changes. // "universe" is a map from file paths to their file contents, // passed through the plugin chain to coordinate file generation. universe := make(map[string]string) universe["src/main.rs"] = fileContent return external.PluginResponse{ Command: "init", Universe: universe, } } func returnError(err error) { response := external.PluginResponse{ Error: true, ErrorMsgs: []string{err.Error()}, } output, marshalErr := json.Marshal(response) if marshalErr != nil { fmt.Fprintf(os.Stderr, "failed to marshal error response: %v\n", marshalErr) os.Exit(1) } fmt.Printf("%s", output) } ``` ## Adapting for Other Languages To support other file extensions, modify the marker implementation by changing: - The comment syntax (e.g., `//` for Java, `#` for Python, `{{/* ... */}}` for templates) - The file extension check (e.g., `.java`, `.py`, `.tpl`) - The marker prefix (e.g., `+java:scaffold:`, `+python:scaffold:`) For more information on creating external plugins, see [External Plugins](external-plugins.md). ================================================ FILE: docs/book/src/plugins/extending/extending_cli_features_and_plugins.md ================================================ # Extending CLI Features and Plugins Kubebuilder provides an extensible architecture to scaffold projects using plugins. These plugins allow you to customize the CLI behavior or integrate new features. In this guide, we’ll explore how to extend CLI features, create custom plugins, and bundle multiple plugins. ## Creating Custom Plugins To create a custom plugin, you need to implement the [Kubebuilder Plugin interface][plugin-interface]. This interface allows your plugin to hook into Kubebuilder’s commands (`init`, `create api`, `create webhook`, etc.) and add custom logic. ### Example of a Custom Plugin You can create a plugin that generates both language-specific scaffolds and the necessary configuration files, using the [Bundle Plugin](#bundle-plugin). This example shows how to combine the Golang plugin with a Kustomize plugin: ```go import ( kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2" golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4" ) mylanguagev1Bundle, _ := plugin.NewBundle( plugin.WithName("mylanguage.kubebuilder.io"), plugin.WithVersion(plugin.Version{Number: 1}), plugin.WithPlugins(kustomizecommonv2.Plugin{}, mylanguagev1.Plugin{}), ) ``` This composition allows you to scaffold a common configuration base (via Kustomize) and the language-specific files (via `mylanguagev1`). You can also use your plugin to scaffold specific resources like CRDs and controllers, using the `create api` and `create webhook` subcommands. ### Plugin Subcommands Plugins are responsible for implementing the code that will be executed when the sub-commands are called. You can create a new plugin by implementing the [Plugin interface][plugin-interface]. On top of being a `Base`, a plugin should also implement the [`SubcommandMetadata`][plugin-subc-metadata] interface so it can be run with a CLI. Optionally, a custom help text for the target command can be set; this method can be a no-op, which will preserve the default help text set by the [cobra][cobra] command constructors. Kubebuilder CLI plugins wrap scaffolding and CLI features in conveniently packaged Go types that are executed by the `kubebuilder` binary, or any binary which imports them. More specifically, a plugin configures the execution of one of the following CLI commands: - `init`: Initializes the project structure. - `create api`: Scaffolds a new API and controller. - `create webhook`: Scaffolds a new webhook. - `edit`: edit the project structure. Here’s an example of using the `init` subcommand with a custom plugin: ```sh kubebuilder init --plugins=mylanguage.kubebuilder.io/v1 ``` This would initialize a project using the `mylanguage` plugin. ### Plugin Keys Plugins are identified by a key of the form `/`. There are two ways to specify a plugin to run: - Setting `kubebuilder init --plugins=`, which will initialize a project configured for plugin with key ``. - A `layout: ` in the scaffolded [PROJECT configuration file][project-file-config]. Commands (except for `init`, which scaffolds this file) will look at this value before running to choose which plugin to run. By default, `` will be `go.kubebuilder.io/vX`, where `X` is some integer. For a full implementation example, check out Kubebuilder's native [`go.kubebuilder.io`][kb-go-plugin] plugin. ### Plugin naming Plugin names must be DNS1123 labels and should be fully qualified, i.e. they have a suffix like `.example.com`. For example, the base Go scaffold used with `kubebuilder` commands has name `go.kubebuilder.io`. Qualified names prevent conflicts between plugin names; both `go.kubebuilder.io` and `go.example.com` can both scaffold Go code and can be specified by a user. ### Plugin versioning A plugin's `Version()` method returns a [`plugin.Version`][plugin-version-type] object containing an integer value and optionally a stage string of either "alpha" or "beta". The integer denotes the current version of a plugin. Two different integer values between versions of plugins indicate that the two plugins are incompatible. The stage string denotes plugin stability: - `alpha`: should be used for plugins that are frequently changed and may break between uses. - `beta`: should be used for plugins that are only changed in minor ways, ex. bug fixes. ### Boilerplates The Kubebuilder internal plugins use boilerplates to generate the files of code. Kubebuilder uses templating to scaffold files for plugins. For instance, when creating a new project, the `go/v4` plugin scaffolds the `go.mod` file using a template defined in its implementation. You can extend this functionality in your custom plugin by defining your own templates and using [Kubebuilder’s machinery library][machinery] to generate files. This library allows you to: - Define file I/O behaviors. - Add [markers][markers-scaffold] to the scaffolded files. - Specify templates for your scaffolds. #### Example: Boilerplate For instance, the go/v4 scaffolds the `go.mod` file by defining an object that [implements the machinery interface][machinery]. The raw template is set to the `TemplateBody` field on the `Template.SetTemplateDefaults` method: ```go {{#include ./../../../../../pkg/plugins/golang/v4/scaffolds/internal/templates/gomod.go}} ``` Such object that implements the machinery interface will later pass to the execution of scaffold: ```go // Scaffold implements cmdutil.Scaffolder func (s *initScaffolder) Scaffold() error { log.Println("Writing scaffold for you to edit...") // Initialize the machinery.Scaffold that will write the boilerplate file to disk // The boilerplate file needs to be scaffolded as a separate step as it is going to // be used by the rest of the files, even those scaffolded in this command call. scaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config), ) ... return scaffold.Execute( ... &templates.GoMod{ ControllerRuntimeVersion: ControllerRuntimeVersion, }, ... ) } ``` #### Example: Overwriting a File in a Plugin Let's imagine that when a subcommand is called, you want to overwrite an existing file. For example, to modify the `Makefile` and add custom build steps, in the definition of your Template you can use the following option: ```go f.IfExistsAction = machinery.OverwriteFile ``` By using those options, your plugin can take control of certain files generated by Kubebuilder’s default scaffolds. ## Customizing Existing Scaffolds Kubebuilder provides utility functions to help you modify the default scaffolds. By using the [plugin utilities][plugin-utils], you can insert, replace, or append content to files generated by Kubebuilder, giving you full control over the scaffolding process. These utilities allow you to: - **Insert content**: Add content at a specific location within a file. - **Replace content**: Search for and replace specific sections of a file. - **Append content**: Add content to the end of a file without removing or altering the existing content. ### Example If you need to insert custom content into a scaffolded file, you can use the `InsertCode` function provided by the plugin utilities: ```go pluginutil.InsertCode(filename, target, code) ``` This approach enables you to extend and modify the generated scaffolds while building custom plugins. For more details, refer to the [Kubebuilder plugin utilities][kb-utils]. ## Bundle Plugin Plugins can be bundled to compose more complex scaffolds. A plugin bundle is a composition of multiple plugins that are executed in a predefined order. For example: ```go myPluginBundle, _ := plugin.NewBundle( plugin.WithName("myplugin.example.com"), plugin.WithVersion(plugin.Version{Number: 1}), plugin.WithPlugins(pluginA.Plugin{}, pluginB.Plugin{}, pluginC.Plugin{}), ) ``` This bundle will execute the `init` subcommand for each plugin in the specified order: 1. `pluginA` 2. `pluginB` 3. `pluginC` The following command will run the bundled plugins: ```sh kubebuilder init --plugins=myplugin.example.com/v1 ``` ## CLI system Plugins are run using a [`CLI`][cli] object, which maps a plugin type to a subcommand and calls that plugin's methods. For example, writing a program that injects an `Init` plugin into a `CLI` then calling `CLI.Run()` will call the plugin's [SubcommandMetadata][plugin-sub-command], [UpdatesMetadata][plugin-update-meta] and `Run` methods with information a user has passed to the program in `kubebuilder init`. Following an example: ```go package cli import ( log "log/slog" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/cli" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang" deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1" golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4" ) var ( // The following is an example of the commands // that you might have in your own binary commands = []*cobra.Command{ myExampleCommand.NewCmd(), } alphaCommands = []*cobra.Command{ myExampleAlphaCommand.NewCmd(), } ) // GetPluginsCLI returns the plugins based CLI configured to be used in your CLI binary func GetPluginsCLI() (*cli.CLI) { // Bundle plugin which built the golang projects scaffold by Kubebuilder go/v4 gov3Bundle, _ := plugin.NewBundleWithOptions(plugin.WithName(golang.DefaultNameQualifier), plugin.WithVersion(plugin.Version{Number: 3}), plugin.WithPlugins(kustomizecommonv2.Plugin{}, golangv4.Plugin{}), ) c, err := cli.New( // Add the name of your CLI binary cli.WithCommandName("example-cli"), // Add the version of your CLI binary cli.WithVersion(versionString()), // Register the plugins options which can be used to do the scaffolds via your CLI tool. See that we are using as example here the plugins which are implemented and provided by Kubebuilder cli.WithPlugins( gov3Bundle, &deployimagev1alpha1.Plugin{}, ), // Defines what will be the default plugin used by your binary. It means that will be the plugin used if no info be provided such as when the user runs `kubebuilder init` cli.WithDefaultPlugins(cfgv3.Version, gov3Bundle), // Define the default project configuration version which will be used by the CLI when none is informed by --project-version flag. cli.WithDefaultProjectVersion(cfgv3.Version), // Adds your own commands to the CLI cli.WithExtraCommands(commands...), // Add your own alpha commands to the CLI cli.WithExtraAlphaCommands(alphaCommands...), // Adds the completion option for your CLI cli.WithCompletion(), ) if err != nil { log.Fatal(err) } return c } // versionString returns the CLI version func versionString() string { // return your binary project version } ``` This program can then be built and run in the following ways: Default behavior: ```sh # Initialize a project with the default Init plugin, "go.example.com/v1". # This key is automatically written to a PROJECT config file. $ my-bin-builder init # Create an API and webhook with "go.example.com/v1" CreateAPI and # CreateWebhook plugin methods. This key was read from the config file. $ my-bin-builder create api [flags] $ my-bin-builder create webhook [flags] ``` Selecting a plugin using `--plugins`: ```sh # Initialize a project with the "ansible.example.com/v1" Init plugin. # Like above, this key is written to a config file. $ my-bin-builder init --plugins ansible # Create an API and webhook with "ansible.example.com/v1" CreateAPI # and CreateWebhook plugin methods. This key was read from the config file. $ my-bin-builder create api [flags] $ my-bin-builder create webhook [flags] ``` ### Inputs should be tracked in the PROJECT file The CLI is responsible for managing the [PROJECT file configuration][project-file-config], which represents the configuration of the projects scaffolded by the CLI tool. When extending Kubebuilder, it is recommended to ensure that your tool or [External Plugin][external-plugin] properly uses the [PROJECT file][project-file-config] to track relevant information. This ensures that other external tools and plugins can properly integrate with the project. It also allows tools features to help users re-scaffold their projects such as using the [Alpha Commands](./../../reference/alpha_commands.md) to upgrade the project scaffold to a newer version of Kubebuilder, ensuring the tracked information in the PROJECT file can be leveraged for various purposes. For example, plugins can check whether they support the project setup and re-execute commands based on the tracked inputs. #### Example By running the following command to use the [Deploy Image][deploy-image] plugin to scaffold an API and its controller: ```sh kubebyilder create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:memcached:1.6.26-alpine3.19 --image-container-command="memcached,--memory-limit=64,-o,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha" --make=false ``` The following entry would be added to the PROJECT file: ```yaml ... plugins: deploy-image.go.kubebuilder.io/v1-alpha: resources: - domain: testproject.org group: example.com kind: Memcached options: containerCommand: memcached,--memory-limit=64,-o,modern,-v containerPort: "11211" image: memcached:memcached:1.6.26-alpine3.19 runAsUser: "1001" version: v1alpha1 - domain: testproject.org group: example.com kind: Busybox options: image: busybox:1.36.1 version: v1alpha1 ... ``` By inspecting the PROJECT file, it becomes possible to understand how the plugin was used and what inputs were provided. This not only allows re-execution of the command based on the tracked data but also enables creating features or plugins that can rely on this information. [sdk]: https://github.com/operator-framework/operator-sdk [plugin-interface]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin [machinery]: https://github.com/kubernetes-sigs/kubebuilder/tree/master/pkg/machinery [plugin-subc-metadata]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin#SubcommandMetadata [plugin-version-type]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin#Version [bundle-plugin-doc]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin#Bundle [deprecate-plugin-doc]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin#Deprecated [plugin-sub-command]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin#Subcommand [plugin-update-meta]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin#UpdatesMetadata [plugin-utils]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin/util [markers-scaffold]: ./../../reference/markers/scaffold.md [kb-utils]: https://github.com/kubernetes-sigs/kubebuilder/blob/book-v4/pkg/plugin/util/util.go [project-file-config]: ./../../reference/project-config.md [cli]: https://github.com/kubernetes-sigs/kubebuilder/tree/book-v4/pkg/cli [kb-go-plugin]: https://github.com/kubernetes-sigs/kubebuilder/tree/book-v4/pkg/plugins/golang/v4 [cobra]: https://github.com/spf13/cobra [external-plugin]: external-plugins.md [deploy-image]: ./../available/deploy-image-plugin-v1-alpha.md [upgrade-assistant]: ./../../reference/rescaffold.md ================================================ FILE: docs/book/src/plugins/extending/external-plugins.md ================================================ # Creating External Plugins for Kubebuilder ## Overview Kubebuilder's functionality can be extended through external plugins. These plugins are executables (written in any language) that follow an execution pattern recognized by Kubebuilder. Kubebuilder interacts with these plugins via `stdin` and `stdout`, enabling seamless communication. ## Why Use External Plugins? External plugins enable third-party solution maintainers to integrate their tools with Kubebuilder. Much like Kubebuilder's own plugins, these can be opt-in, offering users flexibility in tool selection. By developing plugins in their repositories, maintainers ensure updates are aligned with their CI pipelines and can manage any changes within their domain of responsibility. If you are interested in this type of integration, collaborating with the maintainers of the third-party solution is recommended. Kubebuilder's maintainers are always willing to provide support in extending its capabilities. ## How to Write an External Plugin Communication between Kubebuilder and an external plugin occurs via standard I/O. Any language can be used to create the plugin, as long as it follows the [PluginRequest][code-plugin-external] and [PluginResponse][code-plugin-external] structures. `PluginRequest` contains the data collected from the CLI and any previously executed plugins. Kubebuilder sends this data as a JSON object to the external plugin via `stdin`. **Fields:** - `apiVersion`: Version of the PluginRequest schema. - `args`: Command-line arguments passed to the plugin. - `command`: The subcommand being executed (e.g., `init`, `create api`, `create webhook`, `edit`). - `universe`: Map of file paths to contents, updated across the plugin chain. - `pluginChain` (optional): Array of plugin keys in the order they were executed. External plugins can inspect this to tailor behavior based on other plugins that ran (for example, `go.kubebuilder.io/v4` or `kustomize.common.kubebuilder.io/v2`). - `config` (optional): Serialized PROJECT file configuration for the current project. Use it to inspect metadata, existing resources, or plugin-specific settings. Kubebuilder omits this field before the PROJECT file exists—typically during the first `init`—so plugins should check for its presence. **Note:** Whenever Kubebuilder has a PROJECT file available (for example during `create api`, `create webhook`, `edit`, or a subsequent `init` run), `PluginRequest` includes the `config` field. During the very first `init` run the field is omitted because the PROJECT file does not exist yet. **Example `PluginRequest` (triggered by `kubebuilder init --plugins go/v4,sampleexternalplugin/v1 --domain my.domain`):** ```json { "apiVersion": "v1alpha1", "args": ["--domain", "my.domain"], "command": "init", "universe": {}, "pluginChain": ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2", "sampleexternalplugin/v1"] } ``` **Example `PluginRequest` for `create api` (includes `config`):** ```json { "apiVersion": "v1alpha1", "args": ["--group", "crew", "--version", "v1", "--kind", "Captain"], "command": "create api", "universe": {}, "pluginChain": ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2", "sampleexternalplugin/v1"], "config": { "domain": "my.domain", "repo": "github.com/example/my-project", "projectName": "my-project", "version": "3", "layout": ["go.kubebuilder.io/v4"], "multigroup": false, "resources": [] } } ``` **Example `PluginRequest` (triggered by `kubebuilder edit --plugins sampleexternalplugin/v1`):** ```json { "apiVersion": "v1alpha1", "args": [], "command": "edit", "universe": {} } ``` ### PluginResponse `PluginResponse` contains the modifications made by the plugin to the project. This data is serialized as JSON and returned to Kubebuilder through `stdout`. **Example `PluginResponse`:** ```json { "apiVersion": "v1alpha1", "command": "edit", "metadata": { "description": "The `edit` subcommand adds Prometheus instance configuration for monitoring your operator.", "examples": "kubebuilder edit --plugins sampleexternalplugin/v1" }, "universe": { "config/prometheus/prometheus.yaml": "# Prometheus CR manifest...", "config/prometheus/kustomization.yaml": "resources:\n - prometheus.yaml\n", "config/default/kustomization_prometheus_patch.yaml": "# Instructions for enabling Prometheus..." }, "error": false, "errorMsgs": [] } ``` ## How to Use an External Plugin ### Prerequisites - Kubebuilder CLI version > 3.11.0 - An executable for the external plugin - Plugin path configuration using `${EXTERNAL_PLUGINS_PATH}` or default OS-based paths: - Linux: `$HOME/.config/kubebuilder/plugins/${name}/${version}/${name}` - macOS: `~/Library/Application Support/kubebuilder/plugins/${name}/${version}/${name}` **Example:** For a plugin `foo.acme.io` version `v2` on Linux, the path would be `$HOME/.config/kubebuilder/plugins/foo.acme.io/v2/foo.acme.io`. ### Available Subcommands External plugins can support the following Kubebuilder subcommands: - `init`: Project initialization - `create api`: Scaffold Kubernetes API definitions - `create webhook`: Scaffold Kubernetes webhooks - `edit`: Update project configuration **Optional subcommands for enhanced user experience:** - `metadata`: Provide plugin descriptions and examples with the `--help` flag. - `flags`: Inform Kubebuilder of supported flags, enabling early error detection. ### Configuring Plugin Path Set the environment variable `$EXTERNAL_PLUGINS_PATH` to specify a custom plugin binary path: ```sh export EXTERNAL_PLUGINS_PATH= ``` Otherwise, Kubebuilder would search for the plugins in a default path based on your OS. ### Example CLI Commands You can now use it by calling the CLI commands: ```sh # Add Prometheus monitoring to an existing project kubebuilder edit --plugins sampleexternalplugin/v1 # Update an existing project with Prometheus monitoring kubebuilder edit --plugins sampleexternalplugin/v1 # Display help information for the init subcommand kubebuilder init --plugins sampleexternalplugin/v1 --help # Display help information for the edit subcommand kubebuilder edit --plugins sampleexternalplugin/v1 --help # Plugin chaining example: Use go/v4 plugin first, then apply external plugin kubebuilder edit --plugins go/v4,sampleexternalplugin/v1 ``` ## Further resources - A [sample external plugin written in Go](https://github.com/kubernetes-sigs/kubebuilder/tree/master/docs/book/src/simple-external-plugin-tutorial/testdata/sampleexternalplugin/v1) - A [sample external plugin written in Python](https://github.com/rashmigottipati/POC-Phase2-Plugins) - A [sample external plugin written in JavaScript](https://github.com/Eileen-Yu/kb-js-plugin) [code-plugin-external]: https://github.com/kubernetes-sigs/kubebuilder/blob/book-v4/pkg/plugin/external/types.go ================================================ FILE: docs/book/src/plugins/extending/testing-plugins.md ================================================ # Write E2E Tests You can check the [Kubebuilder/v4/test/e2e/utils][utils-kb] package, which offers `TestContext` with rich methods: - [NewTestContext][new-context] helps define: - A temporary folder for testing projects. - A temporary controller-manager image. - The [Kubectl execution method][kubectl-ktc]. - The CLI executable (whether `kubebuilder`, `operator-sdk`, or your extended CLI). Once defined, you can use `TestContext` to: 1. **Setup the testing environment**, e.g.: - Clean up the environment and create a temporary directory. See [Prepare][prepare-method]. - Install prerequisite CRDs. See [InstallCertManager][cert-manager-install], [InstallPrometheusManager][prometheus-manager-install]. 2. **Validate the plugin behavior**, e.g.: - Trigger the plugin's bound subcommands. See [Init][init-subcommand], [CreateAPI][create-api-subcommand]. - Use [PluginUtil][plugin-util] to verify scaffolded outputs. See [InsertCode][insert-code], [ReplaceInFile][replace-in-file], [UncommentCode][uncomment-code]. 3. **Ensure the scaffolded output works**, e.g.: - Execute commands in your `Makefile`. See [Make][make-command]. - Temporarily load an image of the testing controller. See [LoadImageToKindCluster][load-image-to-kind]. - Call Kubectl to validate running resources. See [Kubectl][kubectl-ktc]. 4. **Cleanup temporary resources after testing**: - Uninstall prerequisite CRDs. See [UninstallPrometheusOperManager][uninstall-prometheus-manager]. - Delete the temporary directory. See [Destroy][destroy-method]. **References**: - [operator-sdk e2e tests][sdk-e2e-tests] - [kubebuilder e2e tests][kb-e2e-tests] ## Generate Test Samples It's straightforward to view the content of sample projects generated by your plugin. For example, Kubebuilder generates [sample projects][kb-samples] based on different plugins to validate the layouts. You can also use `TestContext` to generate folders of scaffolded projects from your plugin. The commands are similar to those mentioned in [Extending CLI Features and Plugins][extending-cli]. Here’s a general workflow to create a sample project using the `go/v4` plugin (`kbc` is an instance of `TestContext`): - **To initialize a project**: ```go By("initializing a project") err = kbc.Init( "--plugins", "go/v4", "--project-version", "3", "--domain", kbc.Domain, "--fetch-deps=false", ) Expect(err).NotTo(HaveOccurred(), "Failed to initialize a project") ``` - **To define API:** ```go By("creating API definition") err = kbc.CreateAPI( "--group", kbc.Group, "--version", kbc.Version, "--kind", kbc.Kind, "--namespaced", "--resource", "--controller", "--make=false", ) Expect(err).NotTo(HaveOccurred(), "Failed to create an API") ``` - **To scaffold webhook configurations:** ```go By("scaffolding mutating and validating webhooks") err = kbc.CreateWebhook( "--group", kbc.Group, "--version", kbc.Version, "--kind", kbc.Kind, "--defaulting", "--programmatic-validation", ) Expect(err).NotTo(HaveOccurred(), "Failed to create an webhook") ``` [cert-manager-install]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.InstallCertManager [create-api-subcommand]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.CreateAPI [destroy-method]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.Destroy [extending-cli]: ./extending_cli_features_and_plugins.md [init-subcommand]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.Init [insert-code]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin/util#InsertCode [kb-e2e-tests]: https://github.com/kubernetes-sigs/kubebuilder/tree/book-v4/test/e2e [kb-samples]: https://github.com/kubernetes-sigs/kubebuilder/tree/book-v4/testdata [kubectl-ktc]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#Kubectl [load-image-to-kind]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.LoadImageToKindCluster [make-command]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.Make [new-context]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#NewTestContext [plugin-util]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin/util [prepare-method]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.Prepare [prometheus-manager-install]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.InstallPrometheusOperManager [replace-in-file]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin/util#ReplaceInFile [sdk-e2e-tests]: https://github.com/operator-framework/operator-sdk/tree/master/test/e2e/go [uncomment-code]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/pkg/plugin/util#UncommentCode [uninstall-prometheus-manager]: https://pkg.go.dev/sigs.k8s.io/kubebuilder/v4/test/e2e/utils#TestContext.UninstallPrometheusOperManager [utils-kb]: https://github.com/kubernetes-sigs/kubebuilder/tree/book-v4/test/e2e/utils ================================================ FILE: docs/book/src/plugins/extending.md ================================================ # Extending Kubebuilder Kubebuilder provides an extensible architecture to scaffold projects using plugins. These plugins allow you to customize the CLI behavior or integrate new features. ## Overview Kubebuilder’s CLI can be extended through custom plugins, allowing you to: - Build new scaffolds. - Enhance existing ones. - Add new commands and functionality to Kubebuilder’s scaffolding. This flexibility enables you to create custom project setups tailored to specific needs. ## Options to Extend Extending Kubebuilder can be achieved in two main ways: 1. **Extending CLI features and Plugins**: You can import and build upon existing Kubebuilder plugins to [extend its features and plugins][extending-cli]. This is useful when you need to add specific features to a tool that already benefits from Kubebuilder's scaffolding system. For example, [Operator SDK][sdk] leverages the [kustomize plugin][kustomize-plugin] to provide language support for tools like Ansible or Helm. So that the project can be focused to keep maintained only what is specific language based. 2. **Creating External Plugins**: You can build standalone, independent plugins as binaries. These plugins can be written in any language and should follow an execution pattern that Kubebuilder recognizes. For more information, see [Creating external plugins][external-plugins]. For further details on how to extend Kubebuilder, explore the following sections: - [CLI and Plugins](./extending/extending_cli_features_and_plugins.md) to learn how to extend CLI features and plugins. - [External Plugins](./extending/external-plugins.md) for creating standalone plugins. - [E2E Tests](./extending/testing-plugins.md) to ensure your plugin functions as expected. [extending-cli]: ./extending/extending_cli_features_and_plugins.md [external-plugins]: ./extending/external-plugins.md [sdk]: https://github.com/operator-framework/operator-sdk [kustomize-plugin]: ./available/kustomize-v2.md [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [operator-pattern]: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ ================================================ FILE: docs/book/src/plugins/kustomize-v2.md ================================================ # [Default Scaffold] Kustomize v2 The kustomize plugin allows you to scaffold all kustomize manifests used to work with the language base plugin `base.go.kubebuilder.io/v4`. This plugin is used to generate the manifest under `config/` directory for the projects build within the go/v4 plugin (default scaffold). Note that projects such as [Operator-sdk][sdk] consume the Kubebuilder project as a lib and provide options to work with other languages like Ansible and Helm. The kustomize plugin allows them to easily keep a maintained configuration and ensure that all languages have the same configuration. It is also helpful if you are looking to provide nice plugins which will perform changes on top of what is scaffolded by default. With this approach we do not need to keep manually updating this configuration in all possible language plugins which uses the same and we are also able to create "helper" plugins which can work with many projects and languages. ## When to use it - If you are looking to scaffold the kustomize configuration manifests for your own language plugin - If you are looking for support on Apple Silicon (`darwin/arm64`). (_Before kustomize `4.x` the binary for this plataform is not provided_) - If you are looking for to begin to try out the new syntax and features provide by kustomize v4 [(More info)][release-notes-v4] and v5 [(More info)][release-notes-v5] - If you are NOT looking to build projects which will be used on Kubernetes cluster versions < `1.22` (_The new features provides by kustomize v4 are not officially supported and might not work with kubectl < `1.22`_) - If you are NOT looking to rely on special URLs in resource fields - If you want to use [replacements][kustomize-replacements] since [vars][kustomize-vars] are deprecated and might be removed soon ## How to use it If you are looking to define that your language plugin should use kustomize use the [Bundle Plugin][bundle] to specify that your language plugin is a composition with your plugin responsible for scaffold all that is language specific and kustomize for its configuration, see: ```go import ( ... kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2" golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4" ... ) // Bundle plugin which built the golang projects scaffold by Kubebuilder go/v4 // The follow code is creating a new plugin with its name and version via composition // You can define that one plugin is composite by 1 or Many others plugins gov3Bundle, _ := plugin.NewBundle(plugin.WithName(golang.DefaultNameQualifier), plugin.WithVersion(plugin.Version{Number: 3}), plugin.WithPlugins(kustomizecommonv2.Plugin{}, golangv4.Plugin{}), // scaffold the config/ directory and all kustomize files // Scaffold the Golang files and all that specific for the language e.g. go.mod, apis, controllers ) ``` Also, with Kubebuilder, you can use kustomize/v2 alone via: ```sh kubebuilder init --plugins=kustomize/v2 $ ls -la total 24 drwxr-xr-x 6 camilamacedo86 staff 192 31 Mar 09:56 . drwxr-xr-x 11 camilamacedo86 staff 352 29 Mar 21:23 .. -rw------- 1 camilamacedo86 staff 129 26 Mar 12:01 .dockerignore -rw------- 1 camilamacedo86 staff 367 26 Mar 12:01 .gitignore -rw------- 1 camilamacedo86 staff 94 31 Mar 09:56 PROJECT drwx------ 6 camilamacedo86 staff 192 31 Mar 09:56 config ``` Or combined with the base language plugins: ```sh # Provides the same scaffold of go/v4 plugin which is composition but with kustomize/v2 kubebuilder init --plugins=kustomize/v2,base.go.kubebuilder.io/v4 --domain example.org --repo example.org/guestbook-operator ``` ## Subcommands The kustomize plugin implements the following subcommands: * init (`$ kubebuilder init [OPTIONS]`) * create api (`$ kubebuilder create api [OPTIONS]`) * create webhook (`$ kubebuilder create api [OPTIONS]`) ## Affected files The following scaffolds will be created or updated by this plugin: * `config/*` ## Further resources * Check the kustomize [plugin implementation](https://github.com/kubernetes-sigs/kubebuilder/tree/master/pkg/plugins/common/kustomize) * Check the [kustomize documentation][kustomize-docs] * Check the [kustomize repository][kustomize-github] * Check the [release notes][release-notes-v5] for Kustomize v5.0.0 * Check the [release notes][release-notes-v4] for Kustomuze v4.0.0 * Also, you can compare the `config/` directory between the samples `project-v3` and `project-v4` to check the difference in the syntax of the manifests provided by default [sdk]:https://github.com/operator-framework/operator-sdk [testdata]: https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/ [bundle]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/pkg/plugin/bundle.go [kustomize-create-api]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/pkg/plugins/common/kustomize/v2/scaffolds/api.go#L72-L84 [kustomize-docs]: https://kustomize.io/ [kustomize-github]: https://github.com/kubernetes-sigs/kustomize [kustomize-replacements]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/replacements/ [kustomize-vars]: https://kubectl.docs.kubernetes.io/references/kustomize/kustomization/vars/ [release-notes-v5]: https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv5.0.0 [release-notes-v4]: https://github.com/kubernetes-sigs/kustomize/releases/tag/kustomize%2Fv4.0.0 ================================================ FILE: docs/book/src/plugins/plugins-versioning.md ================================================ # Plugins Versioning | Name | Example | Description | |----------|-----------------------------------------|--------| | Kubebuilder version | `v2.2.0`, `v2.3.0`, `v2.3.1`, `v4.2.0` | Tagged versions of the Kubebuilder project, representing changes to the source code in this repository. See the [releases][kb-releases] page for binary releases. | | Project version | `"1"`, `"2"`, `"3"` | Project version defines the scheme of a `PROJECT` configuration file. This version is defined in a `PROJECT` file's `version`. | | Plugin version | `v2`, `v3`, `v4` | Represents the version of an individual plugin, as well as the corresponding scaffolding that it generates. This version is defined in a plugin key, ex. `go.kubebuilder.io/v2`. See the [design doc][cli-plugins-versioning] for more details. | ### Incrementing versions For more information on how Kubebuilder release versions work, see the [semver][semver] documentation. Project versions should only be increased if a breaking change is introduced in the PROJECT file scheme itself. Changes to the Go scaffolding or the Kubebuilder CLI *do not* affect project version. Similarly, the introduction of a new plugin version might only lead to a new minor version release of Kubebuilder, since no breaking change is being made to the CLI itself. It'd only be a breaking change to Kubebuilder if we remove support for an older plugin version. See the plugins design doc [versioning section][cli-plugins-versioning] for more details on plugin versioning. ## Introducing changes to plugins Changes made to plugins only require a plugin version increase if and only if a change is made to a plugin that breaks projects scaffolded with the previous plugin version. Once a plugin version `vX` is stabilized (it doesn't have an "alpha" or "beta" suffix), a new plugin package should be created containing a new plugin with version `v(X+1)-alpha`. Typically this is done by (semantically) `cp -r pkg/plugins/golang/vX pkg/plugins/golang/v(X+1)` then updating version numbers and paths. All further breaking changes to the plugin should be made in this package; the `vX` plugin would then be frozen to breaking changes. You must also add a migration guide to the [migrations][migrations] section of the Kubebuilder book in your PR. It should detail the steps required for users to upgrade their projects from `vX` to `v(X+1)-alpha`. [semver]: https://semver.org/ [migrations]: ../migrations.md [kb-releases]:https://github.com/kubernetes-sigs/kubebuilder/releases [design-doc]: ./extending [cli-plugins-versioning]:./extending#plugin-versioning ================================================ FILE: docs/book/src/plugins/plugins.md ================================================ # Plugins Kubebuilder's architecture is fundamentally plugin-based. This design enables the Kubebuilder CLI to evolve while maintaining backward compatibility with older versions, allowing users to opt-in or opt-out of specific features, and enabling seamless integration with external tools. By leveraging plugins, projects can extend Kubebuilder and use it as a library to support new functionalities or implement custom scaffolding tailored to their users' needs. This flexibility allows maintainers to build on top of Kubebuilder’s foundation, adapting it to specific use cases while benefiting from its powerful scaffolding engine. Plugins offer several key advantages: - **Backward compatibility**: Ensures older layouts and project structures remain functional with newer versions. - **Customization**: Allows users to opt-in or opt-out for specific features (i.e. [Grafana][grafana-plugin] and [Deploy Image][deploy-image] plugins) - **Extensibility**: Facilitates integration with third-party tools and projects that wish to provide their own [External Plugins][external-plugins], which can be used alongside Kubebuilder to modify and enhance project scaffolding or introduce new features. **For example, to initialize a project with multiple global plugins:** ```sh kubebuilder init --plugins=pluginA,pluginB,pluginC ``` **For example, to apply custom scaffolding using specific plugins:** ```sh kubebuilder create api --plugins=pluginA,pluginB,pluginC OR kubebuilder create webhook --plugins=pluginA,pluginB,pluginC OR kubebuilder edit --plugins=pluginA,pluginB,pluginC ``` This section details the available plugins, how to extend Kubebuilder, and how to create your own plugins while following the same layout structures. - [Available Plugins](./available-plugins.md) - [Extending](./extending.md) - [Plugins Versioning](./plugins-versioning.md) [extending-cli]: extending.md [grafana-plugin]: ./available/grafana-v1-alpha.md [deploy-image]: ./available/deploy-image-plugin-v1-alpha.md [external-plugins]: ./extending/external-plugins.md ================================================ FILE: docs/book/src/plugins/to-add-optional-features.md ================================================ ## To add optional features The following plugins are useful to generate code and take advantage of optional features | Plugin | Key | Description | |-----------------------------------------------------|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [autoupdate.kubebuilder.io/v1-alpha][autoupdate] | `autoupdate/v1-alpha` | Optional helper which scaffolds a scheduled worker that helps keep your project updated with changes in the ecosystem, significantly reducing the burden of manual maintenance. | | [deploy-image.go.kubebuilder.io/v1-alpha][deploy] | `deploy-image/v1-alpha` | Optional helper plugin which can be used to scaffold APIs and controller with code implementation to Deploy and Manage an Operand(image). | | [grafana.kubebuilder.io/v1-alpha][grafana] | `grafana/v1-alpha` | Optional helper plugin which can be used to scaffold Grafana Manifests Dashboards for the default metrics which are exported by controller-runtime. | | [helm.kubebuilder.io/v1-alpha][helm-v1alpha] (deprecated) | `helm/v1-alpha` | **Deprecated** - Optional helper plugin which can be used to scaffold a Helm Chart to distribute the project under the `dist` directory. Use v2-alpha instead. | | [helm.kubebuilder.io/v2-alpha][helm-v2alpha] | `helm/v2-alpha` | Optional helper plugin which dynamically generates Helm charts from kustomize output, preserving all customizations | [grafana]: ./available/grafana-v1-alpha.md [deploy]: ./available/deploy-image-plugin-v1-alpha.md [helm-v1alpha]: ./available/helm-v1-alpha.md [helm-v2alpha]: ./available/helm-v2-alpha.md [autoupdate]: ./available/autoupdate-v1-alpha.md ================================================ FILE: docs/book/src/plugins/to-be-extended.md ================================================ ## To be extended The following plugins are useful for other tools and [External Plugins][external-plugins] which are looking to extend the Kubebuilder functionality. You can use the kustomize plugin, which is responsible for scaffolding the kustomize files under `config/`. The base language plugins are responsible for scaffolding the necessary Golang files, allowing you to create your own plugins for other languages (e.g., [Operator-SDK][sdk] enables users to work with Ansible/Helm) or add additional functionality. For example, [Operator-SDK][sdk] has a plugin which integrates the projects with [OLM][olm] by adding its own features on top. | Plugin | Key | Description | |--------------------------------------------------------|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| | [kustomize.common.kubebuilder.io/v2][kustomize-plugin] | `kustomize/v2` | Responsible for scaffolding all [kustomize][kustomize] files under the `config/` directory | | `base.go.kubebuilder.io/v4` | `base/v4` | Responsible for scaffolding all files which specifically requires Golang. This plugin is used in the composition to create the plugin (`go/v4`) | [kustomize]: https://kustomize.io/ [sdk]: https://github.com/operator-framework/operator-sdk [olm]: https://olm.operatorframework.io/ [kustomize-plugin]: ./available/kustomize-v2.md [external-plugins]: ./extending/external-plugins.md ================================================ FILE: docs/book/src/plugins/to-scaffold-project.md ================================================ ## To scaffold the projects The following plugins are useful to scaffold the whole project with the tool. | Plugin | Key | Description | |--------------------------------------------------------------------------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [go.kubebuilder.io/v4 - (Default scaffold with Kubebuilder init)][go-v4] | `go/v4` | Scaffold composite by `base.go.kubebuilder.io/v4` and [kustomize.common.kubebuilder.io/v2][kustomize-v2]. Responsible for scaffolding Golang projects and its configurations. | [go-v4]: ./available/go-v4-plugin.md [kustomize-v2]: ./available/kustomize-v2.md ================================================ FILE: docs/book/src/quick-start.md ================================================ # Quick Start This Quick Start guide will cover: - [Creating a project](#create-a-project) - [Creating an API](#create-an-api) - [Running locally](#test-it-out) - [Running in-cluster](#run-it-on-the-cluster) ## Prerequisites - [go](https://go.dev/dl/) version v1.24.6+ - [docker](https://docs.docker.com/install/) version 17.03+. - [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) version v1.11.3+. - Access to a Kubernetes v1.11.3+ cluster. ## Installation Install [kubebuilder](https://sigs.k8s.io/kubebuilder): ```bash # download kubebuilder and install locally. curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)" chmod +x kubebuilder && sudo mv kubebuilder /usr/local/bin/ ``` ## Create a Project Create a directory, and then run the init command inside of it to initialize a new project. Follows an example. ```bash mkdir -p ~/projects/guestbook cd ~/projects/guestbook kubebuilder init --domain my.domain --repo my.domain/guestbook ``` ## Create an API Run the following command to create a new API (group/version) as `webapp/v1` and the new Kind(CRD) `Guestbook` on it: ```bash kubebuilder create api --group webapp --version v1 --kind Guestbook ``` **OPTIONAL:** Edit the API definition and the reconciliation business logic. For more info see [Designing an API](/cronjob-tutorial/api-design.md) and [What's in a Controller](cronjob-tutorial/controller-overview.md). If you are editing the API definitions, generate the manifests such as Custom Resources (CRs) or Custom Resource Definitions (CRDs) using ```bash make manifests ```
Click here to see an example. (api/v1/guestbook_types.go)

```go // GuestbookSpec defines the desired state of Guestbook type GuestbookSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // Quantity of instances // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=10 Size int32 `json:"size"` // Name of the ConfigMap for GuestbookSpec's configuration // +kubebuilder:validation:MaxLength=15 // +kubebuilder:validation:MinLength=1 ConfigMapName string `json:"configMapName"` // +kubebuilder:validation:Enum=Phone;Address;Name Type string `json:"type,omitempty"` } // GuestbookStatus defines the observed state of Guestbook type GuestbookStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file // PodName of the active Guestbook node. Active string `json:"active"` // PodNames of the standby Guestbook nodes. Standby []string `json:"standby"` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:scope=Cluster // Guestbook is the Schema for the guestbooks API type Guestbook struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec GuestbookSpec `json:"spec,omitempty"` Status GuestbookStatus `json:"status,omitempty"` } ```

## Test It Out You'll need a Kubernetes cluster to run against. You can use [KinD][kind] to get a local cluster for testing, or run against a remote cluster. Install the CRDs into the cluster: ```bash make install ``` For quick feedback and code-level debugging, run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): ```bash make run ``` ## Install Instances of Custom Resources If you pressed `y` for Create Resource [y/n] then you created a CR for your CRD in your `config/samples/` directory. Edit `config/samples/webapp_v1_guestbook.yaml` to contain a valid `spec`. For example: ```yaml # ... spec: foo: bar ``` Hint: "foo" is a string field defined in `api/v1/guestbook_types.go`: ```go // foo is an example field of Guestbook. Edit guestbook_types.go to remove/update // +optional Foo *string `json:"foo,omitempty"` ``` ```bash kubectl apply -k config/samples/ ``` You can have a look at your applied resource now: ```bash kubectl get guestbooks.webapp.my.domain guestbook-sample -o yaml ``` ## Run It On the Cluster When your controller is ready to be packaged and tested in other clusters. Build and push your image to the location specified by `IMG`: ```bash make docker-build docker-push IMG=/:tag ``` Deploy the controller to the cluster with image specified by `IMG`: ```bash make deploy IMG=/:tag ``` ## Uninstall CRDs To delete your CRDs from the cluster: ```bash make uninstall ``` ## Undeploy controller Undeploy the controller to the cluster: ```bash make undeploy ``` ## Using Plugins Kubebuilder design is based on [Plugins][plugins] and you can use [available plugins][available-plugins] to add optional features to your project. ## Next Steps - [Getting Started Guide][getting-started] (~30 min) - build a solid foundation - [CronJob Tutorial][cronjob-tutorial] - learn by building a demo project - [Groups, Versions, and Kinds][gkv-doc] - understand API design concepts [pre-rbc-gke]: https://cloud.google.com/kubernetes-engine/docs/how-to/role-based-access-control#iam-rolebinding-bootstrap [cronjob-tutorial]: https://book.kubebuilder.io/cronjob-tutorial/cronjob-tutorial.html [GOPATH-golang-docs]: https://go.dev/doc/code.html#GOPATH [go-modules-blogpost]: https://blog.go.dev/using-go-modules [architecture-concept-diagram]: architecture.md [kustomize]: https://github.com/kubernetes-sigs/kustomize [getting-started]: getting-started.md [plugins]: plugins/plugins.md [available-plugins]: plugins/available-plugins.md [envtest]: ./reference/envtest.md [autoupdate-v1-alpha]: plugins/available/autoupdate-v1-alpha.md [deploy-image-v1-alpha]: plugins/available/deploy-image-plugin-v1-alpha.md [gkv-doc]: cronjob-tutorial/gvks.md [kind]: https://sigs.k8s.io/kind [markers]: reference/markers.md [controller-gen]: https://sigs.k8s.io/controller-tools/cmd/controller-gen [scaffolding-markers]: reference/markers/scaffold.md [ai-gh-models]: https://docs.github.com/en/github-models/about-github-models ================================================ FILE: docs/book/src/reference/admission-webhook.md ================================================ # Admission Webhooks [Admission webhooks](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#what-are-admission-webhooks) are HTTP callbacks that receive admission requests, process them and return admission responses. Kubernetes provides the following types of admission webhooks: - **Mutating Admission Webhook**: These can mutate the object while it's being created or updated, before it gets stored. It can be used to default fields in a resource requests, e.g. fields in Deployment that are not specified by the user. It can be used to inject sidecar containers. - **Validating Admission Webhook**: These can validate the object while it's being created or updated, before it gets stored. It allows more complex validation than pure schema-based validation. e.g. cross-field validation and pod image whitelisting. The apiserver by default doesn't authenticate itself to the webhooks. However, if you want to authenticate the clients, you can configure the apiserver to use basic auth, bearer token, or a cert to authenticate itself to the webhooks. You can find detailed steps [here](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#authenticate-apiservers). ## Custom Webhook Paths By default, Kubebuilder generates webhook paths based on the resource's group, version, and kind. For example: - Mutating webhook for `batch/v1/CronJob`: `/mutate-batch-v1-cronjob` - Validating webhook for `batch/v1/CronJob`: `/validate-batch-v1-cronjob` You can specify custom paths for webhooks using dedicated flags: ```bash # Custom path for defaulting webhook kubebuilder create webhook --group batch --version v1 --kind CronJob \ --defaulting --defaulting-path=/my-custom-mutate-path # Custom path for validation webhook kubebuilder create webhook --group batch --version v1 --kind CronJob \ --programmatic-validation --validation-path=/my-custom-validate-path # Both webhooks with different custom paths kubebuilder create webhook --group batch --version v1 --kind CronJob \ --defaulting --programmatic-validation \ --defaulting-path=/custom-mutate --validation-path=/custom-validate ``` ## Handling Resource Status in Admission Webhooks ### Understanding Why: #### Mutating Admission Webhooks Mutating Admission Webhooks are primarily designed to intercept and modify requests concerning the creation, modification, or deletion of objects. Though they possess the capability to modify an object's specification, directly altering its status isn't deemed a standard practice, often leading to unintended results. ```go // MutatingWebhookConfiguration allows for modification of objects. // However, direct modification of the status might result in unexpected behavior. type MutatingWebhookConfiguration struct { ... } ``` #### Setting Initial Status For those diving into custom controllers for custom resources, it's imperative to grasp the concept of setting an initial status. This initialization typically takes place within the controller itself. The moment the controller identifies a new instance of its managed resource, primarily through a watch mechanism, it holds the authority to assign an initial status to that resource. ```go // Custom controller's reconcile function might look something like this: func (r *ReconcileMyResource) Reconcile(request reconcile.Request) (reconcile.Result, error) { // ... // Upon discovering a new instance, set the initial status instance.Status = SomeInitialStatus // ... } ``` #### Status Subresource Delving into Kubernetes custom resources, a clear demarcation exists between the spec (depicting the desired state) and the status (illustrating the observed state). Activating the /status subresource for a custom resource definition (CRD) bifurcates the `status` and `spec`, each assigned to its respective API endpoint. This separation ensures that changes introduced by users, such as modifying the spec, and system-driven updates, like status alterations, remain distinct. Leveraging a mutating webhook to tweak the status during a spec-modifying operation might not pan out as expected, courtesy of this isolation. ```yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: myresources.mygroup.mydomain spec: ... subresources: status: {} # Enables the /status subresource ``` #### Conclusion While certain edge scenarios might allow a mutating webhook to seamlessly modify the status, treading this path isn't a universally acclaimed or recommended strategy. Entrusting the controller logic with status updates remains the most advocated approach. ================================================ FILE: docs/book/src/reference/alpha_commands.md ================================================ # Alpha Commands Kubebuilder provides experimental **alpha commands** to assist with advanced operations such as project migration and scaffold regeneration. These commands are designed to simplify tasks that were previously manual and error-prone by automating or partially automating the process. The following alpha commands are currently available: - [`alpha generate`](./../reference/commands/alpha_generate.md) — Re-scaffold the project using the installed CLI version - [`alpha update`](./../reference/commands/alpha_update.md) — Automate the migration process via 3-way merge using scaffold snapshots For more information, see each command's dedicated documentation. ================================================ FILE: docs/book/src/reference/artifacts.md ================================================ # Artifacts To test your controllers, you will need to use the tarballs containing the required binaries: ```shell ./bin/k8s/ └── 1.25.0-darwin-amd64 ├── etcd ├── kube-apiserver └── kubectl ``` These tarballs are released by [controller-tools](https://github.com/kubernetes-sigs/controller-tools), and you can find the list of available versions at: [envtest-releases.yaml](https://github.com/kubernetes-sigs/controller-tools/blob/main/envtest-releases.yaml). When you run `make envtest` or `make test`, the necessary tarballs are downloaded and properly configured for your project. [env-test-doc]: ./envtest.md [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [controller-gen]: https://github.com/kubernetes-sigs/controller-tools/releases ================================================ FILE: docs/book/src/reference/commands/alpha_generate.md ================================================ # Regenerate your project with (`alpha generate`) ## Overview The `kubebuilder alpha generate` command re-scaffolds your project using the currently installed CLI and plugin versions. It regenerates the full scaffold based on the configuration specified in your [PROJECT][project-config] file. This allows you to apply the latest layout changes, plugin features, and code generation improvements introduced in newer Kubebuilder releases. You may choose to re-scaffold the project in-place (overwriting existing files) or in a separate directory for diff-based inspection and manual integration. ## When to Use It? You can use `kubebuilder alpha generate` to upgrade your project scaffold when new changes are introduced in Kubebuilder. This includes updates to plugins (for example, `go.kubebuilder.io/v3` → `go.kubebuilder.io/v4`) or the CLI releases (for example, 4.3.1 → latest) . This command is helpful when you want to: - Update your project to use the latest layout or plugin version - Regenerate your project scaffold to include recent changes - Compare the current scaffold with the latest and apply updates manually - Create a clean scaffold for reviewing or testing changes Use this command when you want full control of the upgrade process. It is also useful if your project was created with an older CLI version and does not support `alpha update`. This approach allows you to compare changes between your current branch and upstream scaffold updates (e.g., from the main branch), and helps you overlay custom code atop the new scaffold. ## How to Use It? ### Upgrade your current project to CLI version installed (i.e. latest scaffold) ```sh kubebuilder alpha generate ``` After running this command, your project will be re-scaffolded in place. You can then compare the local changes with your main branch to see what was updated, and re-apply your custom code on top as needed. ### Generate Scaffold to a New Directory Use the `--input-dir` and `--output-dir` flags to specify input and output paths. ```sh kubebuilder alpha generate \ --input-dir=/path/to/existing/project \ --output-dir=/path/to/new/project ``` After running the command, you can inspect the generated scaffold in the specified output directory. ### Flags | Flag | Description | |------------------|-----------------------------------------------------------------------------| | `--input-dir` | Path to the directory containing the `PROJECT` file. Defaults to CWD. Deletes all files except `.git` and `PROJECT`. | | `--output-dir` | Directory where the new scaffold will be written. If unset, re-scaffolds in-place. | | `--plugins` | Plugin keys to use for this generation. | | `-h, --help` | Show help for this command. | ## Further Resources - [Video demo on how it works](https://youtu.be/7997RIbx8kw?si=ODYMud5lLycz7osp) - [Design proposal documentation](../../../../../designs/helper_to_upgrade_projects_by_rescaffolding.md) [example]: ../../../../../testdata/project-v4-with-plugins/PROJECT [project-config]: ../../reference/project-config.md ================================================ FILE: docs/book/src/reference/commands/alpha_update.md ================================================ # Update Your Project with (`alpha update`) ## Overview `kubebuilder alpha update` upgrades your project’s scaffold to a newer Kubebuilder release using a **3-way Git merge**. It rebuilds clean scaffolds for the old and new versions, merges your current code into the new scaffold, and gives you a reviewable output branch. It takes care of the heavy lifting so you can focus on reviewing and resolving conflicts, not re-applying your code. By default, the final result is **squashed into a single commit** on a dedicated output branch. If you prefer to keep the full history (no squash), use `--show-commits`. ## When to Use It Use this command when you: - Want to move to a newer Kubebuilder version or plugin layout - Want to review scaffold changes on a separate branch - Want to focus on resolving merge conflicts (not re-applying your custom code) ## How It Works You tell the tool the **new version**, and which branch has your project. It rebuilds both scaffolds, merges your code into the new one with a **3-way merge**, and gives you an output branch you can review and merge safely. You decide if you want one clean commit, the full history, or an auto-push to remote. ### Step 1: Detect versions - It looks at your `PROJECT` file or the flags you pass. - Decides which **old version** you are coming from by reading the `cliVersion` field in the `PROJECT` file (if available). - Figures out which **new version** you want (defaults to the latest release). - Chooses which branch has your current code (defaults to `main`). ### Step 2: Create scaffolds The command creates three temporary branches: - **Ancestor**: a clean project scaffold from the **old version**. - **Original**: a snapshot of your **current code**. - **Upgrade**: a clean scaffold from the **new version**. ### Step 3: Do a 3-way merge - Merges **Original** (your code) into **Upgrade** (the new scaffold) using Git’s **3-way merge**. - This keeps your customizations while pulling in upstream changes. - If conflicts happen: - **Default** → stop and let you resolve them manually. - **With `--force`** → continue and commit even with conflict markers. **(ideal for automation)** - Runs `make manifests generate fmt vet lint-fix` to tidy things up. ### Step 4: Write the output branch - By default, everything is **squashed into one commit** on a safe output branch: `kubebuilder-update-from--to-`. - You can change the behavior: - `--show-commits`: keep the full history. - `--restore-path`: in squash mode, restore specific files (like CI configs) from your base branch. - `--output-branch`: pick a custom branch name. - `--merge-message`: customize the commit message for clean merges. - `--conflict-message`: customize the commit message when conflicts occur. - `--push`: push the result to `origin` automatically. - `--git-config`: sets git configurations. - `--open-gh-issue`: create a GitHub issue with a checklist and compare link (requires `gh`). - `--use-gh-models`: add an AI overview **comment** to that issue using `gh models` ### Step 5: Cleanup - Once the output branch is ready, all the temporary working branches are deleted. - You are left with one clean branch you can test, review, and merge back into your main branch. ## How to Use It (commands) Run from your project root: ```shell kubebuilder alpha update ``` Pin versions and base branch: ```shell kubebuilder alpha update \ --from-version v4.5.2 \ --to-version v4.6.0 \ --from-branch main ``` Automation-friendly (proceed even with conflicts): ```shell kubebuilder alpha update --force ``` Keep full history instead of squashing: ``` kubebuilder alpha update --from-version v4.5.0 --to-version v4.7.0 --force --show-commits ``` Default squash but **preserve** CI/workflows from the base branch: ```shell kubebuilder alpha update --force \ --restore-path .github/workflows \ --restore-path docs ``` Use a custom output branch name: ```shell kubebuilder alpha update --force \ --output-branch upgrade/kb-to-v4.7.0 ``` Run update and push the result to origin: ```shell kubebuilder alpha update --from-version v4.6.0 --to-version v4.7.0 --force --push ``` Customize commit messages: ```shell kubebuilder alpha update --force \ --merge-message "chore: upgrade kubebuilder scaffold" \ --conflict-message "chore: upgrade with conflicts - manual review needed" ``` ## Handling Conflicts (`--force` vs default) When you use `--force`, Git finishes the merge even if there are conflicts. The commit will include markers like: ```shell <<<<<<< HEAD Your changes ======= Incoming changes >>>>>>> (original) ``` This allows you to run the command in CI or cron jobs without manual intervention. - Without `--force`: the command stops on the merge branch and prints guidance; no commit is created. - With `--force`: the merge is committed (merge or output branch) and contains the markers. After you fix conflicts, always run: ```shell make manifests generate fmt vet lint-fix # or make all ``` ## Using with GitHub Issues (`--open-gh-issue`) and AI (`--use-gh-models`) assistance Pass `--open-gh-issue` to have the command create a GitHub **Issue** in your repository to assist with the update. Also, if you also pass `--use-gh-models`, the tool posts a follow-up comment on that Issue with an AI-generated overview of the most important changes plus brief conflict-resolution guidance. ### Examples Create an Issue with a compare link: ```shell kubebuilder alpha update --open-gh-issue ``` Create an Issue **and** add an AI summary: ```shell kubebuilder alpha update --open-gh-issue --use-gh-models ``` ### What you’ll see The command opens an Issue that links to the diff so you can create the PR and review it, for example: Example Issue With `--use-gh-models`, an AI comment highlights key changes and suggests how to resolve any conflicts: Comment Moreover, AI models are used to help you understand what changes are needed to keep your project up to date, and to suggest resolutions if conflicts are encountered, as in the following example: ### Automation This integrates cleanly with automation. The [`autoupdate.kubebuilder.io/v1-alpha`][autoupdate-plugin] plugin can scaffold a GitHub Actions workflow that runs the command on a schedule (e.g., weekly). When a new Kubebuilder release is available, it opens an Issue with a compare link so you can create the PR and review it. ## Changing Extra Git configs only during the run (does not change your ~/.gitconfig)_ By default, `kubebuilder alpha update` applies safe Git configs: `merge.renameLimit=999999`, `diff.renameLimit=999999`, `merge.conflictStyle=merge` You can add more, or disable them. - **Add more on top of defaults** ```shell kubebuilder alpha update \ --git-config rerere.enabled=true ``` - **Disable defaults entirely** ```shell kubebuilder alpha update --git-config disable ``` - **Disable defaults and set your own** ```shell kubebuilder alpha update \ --git-config disable \ --git-config rerere.enabled=true ``` ## Flags | Flag | Description | |------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `--conflict-message` | Custom commit message for merges with conflicts. Defaults to `:warning: chore(kubebuilder): update scaffold (manual conflict resolution) -> `. | | `--force` | Continue even if merge conflicts happen. Conflicted files are committed with conflict markers (CI/cron friendly). | | `--from-branch` | Git branch that holds your current project code. Defaults to `main`. | | `--from-version` | Kubebuilder release to update **from** (e.g., `v4.6.0`). If unset, read from the `PROJECT` file when possible. | | `--git-config` | Repeatable. Pass per-invocation Git config as `-c key=value`. **Default** (if omitted): `-c merge.renameLimit=999999 -c diff.renameLimit=999999`. Your configs are applied on top. To disable defaults, include `--git-config disable`. | | `--merge-message` | Custom commit message for successful merges (no conflicts). Defaults to `chore(kubebuilder): update scaffold -> `. | | `--open-gh-issue` | Create a GitHub issue with a pre-filled checklist and compare link after the update completes (requires `gh`). | | `--output-branch` | Name of the output branch. Default: `kubebuilder-update-from--to-`. | | `--push` | Push the output branch to the `origin` remote after the update completes. | | `--restore-path` | Repeatable. Paths to preserve from the base branch when squashing (e.g., `.github/workflows`). **Not supported** with `--show-commits`. | | `--show-commits` | Keep full history (do not squash). **Not compatible** with `--restore-path`. | | `--to-version` | Kubebuilder release to update **to** (e.g., `v4.7.0`). If unset, defaults to the latest available release. | | `--use-gh-models` | Post an AI overview as an issue comment using `gh models`. Requires `gh` + `gh-models` extension. Effective only when `--open-gh-issue` is also set. | | `-h, --help` | Show help for this command. | ## Demonstration ## Further Resources - [AutoUpdate Plugin][autoupdate-plugin] - [Design proposal for update automation][design-proposal] - [Project configuration reference][project-config] [project-config]: ../../reference/project-config.md [autoupdate-plugin]: ./../../plugins/available/autoupdate-v1-alpha.md [design-proposal]: ./../../../../../designs/update_action.md [ai-gh-models]: https://docs.github.com/en/github-models/about-github-models ================================================ FILE: docs/book/src/reference/completion.md ================================================ # Enabling shell autocompletion The Kubebuilder completion script can be generated with the command `kubebuilder completion [bash|fish|powershell|zsh]`. Note that sourcing the completion script in your shell enables Kubebuilder autocompletion. ## Bash - Check that bash is an available shell: ```bash cat /etc/shells | grep '^.*/bash' ``` - If not, add bash to `/etc/shells`. For example, if bash is at `/usr/local/bin/bash`: ```bash echo "/usr/local/bin/bash" >> /etc/shells ``` - Make sure the current user uses bash as their shell. ```bash chsh -s /usr/local/bin/bash ``` - Add following content to `~/.bash_profile` or `~/.bashrc` ```bash # kubebuilder autocompletion if [ -f /usr/local/share/bash-completion/bash_completion ]; then . /usr/local/share/bash-completion/bash_completion fi . <(kubebuilder completion bash) ``` - Restart terminal for the changes to be reflected or `source` the changed bash file. ```bash . ~/.bash_profile ``` ## Zsh Follow a similar protocol for `zsh` completion. ## Fish ``` source (kubebuilder completion fish | psub) ``` ================================================ FILE: docs/book/src/reference/controller-gen.md ================================================ # controller-gen CLI Kubebuilder makes use of a tool called [controller-gen](https://sigs.k8s.io/controller-tools/cmd/controller-gen) for generating utility code and Kubernetes YAML. This code and config generation is controlled by the presence of special ["marker comments"](/reference/markers.md) in Go code. controller-gen is built out of different "generators" (which specify what to generate) and "output rules" (which specify how and where to write the results). Both are configured through command line options specified in [marker format](/reference/markers.md). For instance, the following command: ```shell controller-gen paths=./... crd:trivialVersions=true rbac:roleName=controller-perms output:crd:artifacts:config=config/crd/bases ``` generates CRDs and RBAC, and specifically stores the generated CRD YAML in `config/crd/bases`. For the RBAC, it uses the default output rules (`config/rbac`). It considers every package in the current directory tree (as per the normal rules of the go `...` wildcard). ## Generators Each different generator is configured through a CLI option. Multiple generators may be used in a single invocation of `controller-gen`. {{#markerdocs CLI: generators}} ## Output Rules Output rules configure how a given generator outputs its results. There is always one global "fallback" output rule (specified as `output:`), plus per-generator overrides (specified as `output::`). For brevity, the per-generator output rules (`output::`) are omitted below. They are equivalent to the global fallback options listed here. {{#markerdocs CLI: output rules (optionally as output::...)}} ## Other Options {{#markerdocs CLI: generic}} ================================================ FILE: docs/book/src/reference/crd-scope.md ================================================ # CRD Scope This document explains CustomResourceDefinition (CRD) scope in Kubernetes: how CRDs can be defined as namespace-scoped or cluster-scoped resources. ## Overview CRD scope determines the visibility and availability of custom resources: | Scope | Description | Example Resources | |-------|-------------|-------------------| | **Namespace-scoped** (default) | Resources exist within a specific namespace | Deployments, Services, ConfigMaps, Pods | | **Cluster-scoped** | Resources are global across the entire cluster | Nodes, ClusterRoles, Namespaces, PersistentVolumes | ## Namespace-Scoped CRDs (Default) By default, Kubebuilder creates namespace-scoped CRDs: ```bash kubebuilder create api --group cache --version v1alpha1 --kind Memcached ``` Generated CRD manifest: ```yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: memcacheds.cache.example.com spec: scope: Namespaced # Default group: cache.example.com names: kind: Memcached plural: memcacheds versions: - name: v1alpha1 # ... ``` Custom resources are created in specific namespaces: ```bash kubectl apply -f memcached.yaml -n my-namespace kubectl get memcacheds -n my-namespace ``` **When to use:** - Resources tied to specific applications, teams, or tenants - Multi-tenant environments where isolation is required - Most application-level resources **Considerations:** - Testing new CRD versions requires proper versioning and conversion strategies - Conversion webhooks must account for namespace scope - Facilitates controlled rollout within specific namespaces ## Cluster-Scoped CRDs Cluster-scoped CRDs create resources that are global across the entire cluster. ### Creating Cluster-Scoped CRDs When creating the API, use the `--namespaced=false` flag: ```bash kubebuilder create api --group infrastructure --version v1 --kind Database --namespaced=false ``` Generated CRD manifest: ```yaml apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: databases.infrastructure.example.com spec: scope: Cluster # Cluster-scoped group: infrastructure.example.com names: kind: Database plural: databases versions: - name: v1 # ... ``` Custom resources are cluster-wide (no namespace): ```bash kubectl apply -f database.yaml kubectl get databases # No namespace needed ``` **When to use:** - Resources that are global to the cluster (infrastructure, configuration) - Resources that need to be accessible from all namespaces - Resources that manage cluster-level concerns **Examples:** - Infrastructure configurations (cloud provider settings, cluster DNS) - Global policies or quotas - Cross-namespace resource aggregation ## Changing CRD Scope ### For Existing APIs After creating an API, you can change its scope using the `+kubebuilder:resource:scope` marker: **For cluster-scoped:** ```go //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:scope=Cluster // Database is the Schema for the databases API type Database struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec DatabaseSpec `json:"spec,omitempty"` Status DatabaseStatus `json:"status,omitempty"` } ``` **For namespace-scoped:** ```go //+kubebuilder:object:root=true //+kubebuilder:subresource:status //+kubebuilder:resource:scope=Namespaced // Memcached is the Schema for the memcacheds API type Memcached struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec MemcachedSpec `json:"spec,omitempty"` Status MemcachedStatus `json:"status,omitempty"` } ``` After updating markers, regenerate manifests: ```bash make manifests ``` ## RBAC for CRD Scope ### Namespace-Scoped CRDs Controllers watching namespace-scoped CRDs use namespace-scoped RBAC: ```go //+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch //+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update ``` Generated RBAC (cluster-scoped manager): ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: manager-role rules: - apiGroups: ["cache.example.com"] resources: ["memcacheds"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] ``` Generated RBAC (namespace-scoped manager): ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: manager-role namespace: manager-namespace rules: - apiGroups: ["cache.example.com"] resources: ["memcacheds"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] ``` ### Cluster-Scoped CRDs Controllers watching cluster-scoped CRDs **must** use cluster-wide RBAC: ```go //+kubebuilder:rbac:groups=infrastructure.example.com,resources=databases,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=infrastructure.example.com,resources=databases/status,verbs=get;update;patch //+kubebuilder:rbac:groups=infrastructure.example.com,resources=databases/finalizers,verbs=update ``` Generated RBAC (always ClusterRole for cluster-scoped CRDs): ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: manager-role rules: - apiGroups: ["infrastructure.example.com"] resources: ["databases"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] ``` ## Version Conversion and Webhooks For namespace-scoped CRDs with multiple versions, conversion webhooks must account for namespace scope: ```go //+kubebuilder:webhook:path=/convert,mutating=false,failurePolicy=fail,groups=cache.example.com,resources=memcacheds,verbs=create;update,versions=v1;v1beta1,name=cmemcached.kb.io,sideEffects=None,admissionReviewVersions=v1 ``` The webhook must handle conversion for resources in any namespace. See the [multi-version tutorial](https://book.kubebuilder.io/multiversion-tutorial/tutorial) for details. ## Testing ### Testing Namespace-Scoped CRDs ```bash # Create resource in namespace kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml -n test-namespace # Verify it exists in that namespace only kubectl get memcacheds -n test-namespace kubectl get memcacheds -n other-namespace # Should not find it ``` ### Testing Cluster-Scoped CRDs ```bash # Create cluster-scoped resource (no namespace) kubectl apply -f config/samples/infrastructure_v1_database.yaml # Verify it's cluster-wide kubectl get databases # No namespace needed ``` ## See Also - [Manager Scope](./manager-scope.md) - Configuring manager watching scope - [Generating CRDs](./generating-crd.md) - CRD generation and markers - [Multi-Version Tutorial](https://book.kubebuilder.io/multiversion-tutorial/tutorial) - CRD versioning and conversion - [Kubernetes CRD Documentation](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/) ================================================ FILE: docs/book/src/reference/envtest.md ================================================ # Configuring envtest for integration tests The [`controller-runtime/pkg/envtest`][envtest] Go library helps write integration tests for your controllers by setting up and starting an instance of etcd and the Kubernetes API server, without kubelet, controller-manager or other components. ## Installation Installing the binaries is as a simple as running `make envtest`. `envtest` will download the Kubernetes API server binaries to the `bin/` folder in your project by default. `make test` is the one-stop shop for downloading the binaries, setting up the test environment, and running the tests. You can refer to the Makefile of the Kubebuilder scaffold and observe that the envtest setup is consistently aligned across all controller-runtime releases.Starting from `release-0.19`, it is configured to automatically download the artefact from the correct location, **ensuring that kubebuilder users are not impacted.** ```shell ## Tool Binaries .. ENVTEST ?= $(LOCALBIN)/setup-envtest ... ## Tool Versions ... #ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20) ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime | awk -F'[v.]' '{printf "release-%d.%d", $$2, $$3}') #ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31) ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}') ... .PHONY: setup-envtest setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory. @echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..." @$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \ echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \ exit 1; \ } .PHONY: envtest envtest: $(ENVTEST) ## Download setup-envtest locally if necessary. $(ENVTEST): $(LOCALBIN) $(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION)) ``` ## Installation in Air Gapped/disconnected environments If you would like to download the tarball containing the binaries, to use in a disconnected environment you can use [`setup-envtest`][setup-envtest] to download the required binaries locally. There are a lot of ways to configure `setup-envtest` to avoid talking to the internet you can read about them [here](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest#what-if-i-dont-want-to-talk-to-the-internet). The examples below will show how to install the Kubernetes API binaries using mostly defaults set by `setup-envtest`. ### Download the binaries `make envtest` will download the `setup-envtest` binary to `./bin/`. ```shell make envtest ``` Installing the binaries using `setup-envtest` stores the binary in OS specific locations, you can read more about them [here](https://github.com/kubernetes-sigs/controller-runtime/tree/master/tools/setup-envtest#where-does-it-put-all-those-binaries) ```sh ./bin/setup-envtest use 1.31.0 ``` ### Update the test make target Once these binaries are installed, change the `test` make target to include a `-i` like below. `-i` will only check for locally installed binaries and not reach out to remote resources. You could also set the `ENVTEST_INSTALLED_ONLY` env variable. ```makefile test: manifests generate fmt vet KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -i --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out ``` NOTE: The `ENVTEST_K8S_VERSION` needs to match the `setup-envtest` you downloaded above. Otherwise, you will see an error like the below ```sh no such version (1.24.5) exists on disk for this architecture (darwin/amd64) -- try running `list -i` to see what's on disk ``` ## Writing tests Using `envtest` in integration tests follows the general flow of: ```go import sigs.k8s.io/controller-runtime/pkg/envtest //specify testEnv configuration testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, } //start testEnv cfg, err = testEnv.Start() //write test logic //stop testEnv err = testEnv.Stop() ``` `kubebuilder` does the boilerplate setup and teardown of testEnv for you, in the ginkgo test suite that it generates under the `/controllers` directory. Logs from the test runs are prefixed with `test-env`. ### Configuring your test control plane Controller-runtime’s [envtest][envtest] framework requires `kubectl`, `kube-apiserver`, and `etcd` binaries be present locally to simulate the API portions of a real cluster. The `make test` command will install these binaries to the `bin/` directory and use them when running tests that use `envtest`. Ie, ```shell ./bin/k8s/ └── 1.25.0-darwin-amd64 ├── etcd ├── kube-apiserver └── kubectl ``` You can use environment variables and/or flags to specify the `kubectl`,`api-server` and `etcd` setup within your integration tests. ### Environment Variables | Variable name | Type | When to use | | --- | :--- | :--- | | `USE_EXISTING_CLUSTER` | boolean | Instead of setting up a local control plane, point to the control plane of an existing cluster. | | `KUBEBUILDER_ASSETS` | path to directory | Point integration tests to a directory containing all binaries (api-server, etcd and kubectl). | | `TEST_ASSET_KUBE_APISERVER`, `TEST_ASSET_ETCD`, `TEST_ASSET_KUBECTL` | paths to, respectively, api-server, etcd and kubectl binaries | Similar to `KUBEBUILDER_ASSETS`, but more granular. Point integration tests to use binaries other than the default ones. These environment variables can also be used to ensure specific tests run with expected versions of these binaries. | | `KUBEBUILDER_CONTROLPLANE_START_TIMEOUT` and `KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT` | durations in format supported by [`time.ParseDuration`](https://golang.org/pkg/time/#ParseDuration) | Specify timeouts different from the default for the test control plane to (respectively) start and stop; any test run that exceeds them will fail. | | `KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT` | boolean | Set to `true` to attach the control plane's stdout and stderr to os.Stdout and os.Stderr. This can be useful when debugging test failures, as output will include output from the control plane. | See that the `test` makefile target will ensure that all is properly setup when you are using it. However, if you would like to run the tests without use the Makefile targets, for example via an IDE, then you can set the environment variables directly in the code of your `suite_test.go`: ```go var _ = BeforeSuite(func(done Done) { Expect(os.Setenv("TEST_ASSET_KUBE_APISERVER", "../bin/k8s/1.25.0-darwin-amd64/kube-apiserver")).To(Succeed()) Expect(os.Setenv("TEST_ASSET_ETCD", "../bin/k8s/1.25.0-darwin-amd64/etcd")).To(Succeed()) Expect(os.Setenv("TEST_ASSET_KUBECTL", "../bin/k8s/1.25.0-darwin-amd64/kubectl")).To(Succeed()) // OR Expect(os.Setenv("KUBEBUILDER_ASSETS", "../bin/k8s/1.25.0-darwin-amd64")).To(Succeed()) logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) testenv = &envtest.Environment{} _, err := testenv.Start() Expect(err).NotTo(HaveOccurred()) close(done) }, 60) var _ = AfterSuite(func() { Expect(testenv.Stop()).To(Succeed()) Expect(os.Unsetenv("TEST_ASSET_KUBE_APISERVER")).To(Succeed()) Expect(os.Unsetenv("TEST_ASSET_ETCD")).To(Succeed()) Expect(os.Unsetenv("TEST_ASSET_KUBECTL")).To(Succeed()) }) ``` ### Flags Here's an example of modifying the flags with which to start the API server in your integration tests, compared to the default values in `envtest.DefaultKubeAPIServerFlags`: ```go customApiServerFlags := []string{ "--secure-port=6884", "--admission-control=MutatingAdmissionWebhook", } apiServerFlags := append([]string(nil), envtest.DefaultKubeAPIServerFlags...) apiServerFlags = append(apiServerFlags, customApiServerFlags...) testEnv = &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, KubeAPIServerFlags: apiServerFlags, } ``` ## Testing considerations Unless you're using an existing cluster, keep in mind that no built-in controllers are running in the test context. In some ways, the test control plane will behave differently from "real" clusters, and that might have an impact on how you write tests. One common example is garbage collection; because there are no controllers monitoring built-in resources, objects do not get deleted, even if an `OwnerReference` is set up. To test that the deletion lifecycle works, test the ownership instead of asserting on existence. For example: ```go expectedOwnerReference := v1.OwnerReference{ Kind: "MyCoolCustomResource", APIVersion: "my.api.example.com/v1beta1", UID: "d9607e19-f88f-11e6-a518-42010a800195", Name: "userSpecifiedResourceName", } Expect(deployment.ObjectMeta.OwnerReferences).To(ContainElement(expectedOwnerReference)) ``` ## Cert-Manager and Prometheus options Projects scaffolded with Kubebuilder can enable the [`metrics`][metrics] and the [`cert-manager`][cert-manager] options. Note that when we are using the ENV TEST we are looking to test the controllers and their reconciliation. It is considered an integrated test because the ENV TEST API will do the test against a cluster and because of this the binaries are downloaded and used to configure its pre-requirements, however, its purpose is mainly to `unit` test the controllers. Therefore, to test a reconciliation in common cases you do not need to care about these options. However, if you would like to do tests with the Prometheus and the Cert-manager installed you can add the required steps to install them before running the tests. Following an example. ```go // Add the operations to install the Prometheus operator and the cert-manager // before the tests. BeforeEach(func() { By("installing prometheus operator") Expect(utils.InstallPrometheusOperator()).To(Succeed()) By("installing the cert-manager") Expect(utils.InstallCertManager()).To(Succeed()) }) // You can also remove them after the tests:: AfterEach(func() { By("uninstalling the Prometheus manager bundle") utils.UninstallPrometheusOperManager() By("uninstalling the cert-manager bundle") utils.UninstallCertManager() }) ``` Check the following example of how you can implement the above operations: ```go const ( certmanagerVersion = "v1.5.3" certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml" defaultKindCluster = "kind" defaultKindBinary = "kind" prometheusOperatorVersion = "0.51" prometheusOperatorURL = "https://raw.githubusercontent.com/prometheus-operator/" + "prometheus-operator/release-%s/bundle.yaml" ) func warnError(err error) { _, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err) } // InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. func InstallPrometheusOperator() error { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "apply", "-f", url) _, err := Run(cmd) return err } // UninstallPrometheusOperator uninstalls the prometheus func UninstallPrometheusOperator() { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } } // UninstallCertManager uninstalls the cert manager func UninstallCertManager() { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } } // InstallCertManager installs the cert manager bundle. func InstallCertManager() error { url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion) cmd := exec.Command("kubectl", "apply", "-f", url) if _, err := Run(cmd); err != nil { return err } // Wait for cert-manager-webhook to be ready, which can take time if cert-manager //was re-installed after uninstalling on a cluster. cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook", "--for", "condition=Available", "--namespace", "cert-manager", "--timeout", "5m", ) _, err := Run(cmd) return err } // LoadImageToKindClusterWithName loads a local docker image to the kind cluster func LoadImageToKindClusterWithName(name string) error { cluster := defaultKindCluster if v, ok := os.LookupEnv("KIND_CLUSTER"); ok { cluster = v } kindOptions := []string{"load", "docker-image", name, "--name", cluster} kindBinary := defaultKindBinary if v, ok := os.LookupEnv("KIND"); ok { kindBinary = v } cmd := exec.Command(kindBinary, kindOptions...) _, err := Run(cmd) return err } ``` However, see that tests for the metrics and cert-manager might fit better well as e2e tests and not under the tests done using ENV TEST for the controllers. You might want to give a look at the [sample example][sdk-e2e-sample-example] implemented into [Operator-SDK][sdk] repository to know how you can write your e2e tests to ensure the basic workflows of your project. Also, see that you can run the tests against a cluster where you have some configurations in place they can use the option to test using an existing cluster: ```go testEnv = &envtest.Environment{ UseExistingCluster: true, } ``` [metrics]: https://book.kubebuilder.io/reference/metrics.html [envtest]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/envtest [setup-envtest]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/tools/setup-envtest [cert-manager]: https://book.kubebuilder.io/cronjob-tutorial/cert-manager.html [sdk-e2e-sample-example]: https://github.com/operator-framework/operator-sdk/tree/master/testdata/go/v4/memcached-operator/test/e2e [sdk]: https://github.com/operator-framework/operator-sdk [readme]: https://github.com/kubernetes-sigs/controller-runtime/blob/main/tools/setup-envtest/README.md ================================================ FILE: docs/book/src/reference/generating-crd.md ================================================ # Generating CRDs Kubebuilder uses a tool called [`controller-gen`][controller-tools] to generate utility code and Kubernetes object YAML, like CustomResourceDefinitions. To do this, it makes use of special "marker comments" (comments that start with `// +`) to indicate additional information about fields, types, and packages. In the case of CRDs, these are generally pulled from your `_types.go` files. For more information on markers, see the [marker reference docs][marker-ref]. Kubebuilder provides a `make` target to run controller-gen and generate CRDs: `make manifests`. When you run `make manifests`, you should see CRDs generated under the `config/crd/bases` directory. `make manifests` can generate a number of other artifacts as well -- see the [marker reference docs][marker-ref] for more details. ## Validation CRDs support [declarative validation][kube-validation] using an [OpenAPI v3 schema][openapi-schema] in the `validation` section. In general, [validation markers](./markers/crd-validation.md) may be attached to fields or to types. If you're defining complex validation, if you need to re-use validation, or if you need to validate slice elements, it's often best to define a new type to describe your validation. For example: ```go type ToySpec struct { // +kubebuilder:validation:MaxLength=15 // +kubebuilder:validation:MinLength=1 Name string `json:"name,omitempty"` // +kubebuilder:validation:MaxItems=500 // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:UniqueItems=true Knights []string `json:"knights,omitempty"` Alias Alias `json:"alias,omitempty"` Rank Rank `json:"rank"` } // +kubebuilder:validation:Enum=Lion;Wolf;Dragon type Alias string // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=3 // +kubebuilder:validation:ExclusiveMaximum=false type Rank int32 ``` ## Additional Printer Columns Starting with Kubernetes 1.11, `kubectl get` can ask the server what columns to display. For CRDs, this can be used to provide useful, type-specific information with `kubectl get`, similar to the information provided for built-in types. The information that gets displayed can be controlled with the [additionalPrinterColumns field][kube-additional-printer-columns] on your CRD, which is controlled by the [`+kubebuilder:printcolumn`][crd-markers] marker on the Go type for your CRD. For instance, in the following example, we add fields to display information about the knights, rank, and alias fields from the validation example: ```go // +kubebuilder:printcolumn:name="Alias",type=string,JSONPath=`.spec.alias` // +kubebuilder:printcolumn:name="Rank",type=integer,JSONPath=`.spec.rank` // +kubebuilder:printcolumn:name="Bravely Run Away",type=boolean,JSONPath=`.spec.knights[?(@ == "Sir Robin")]`,description="when danger rears its ugly head, he bravely turned his tail and fled",priority=10 // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" type Toy struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ToySpec `json:"spec,omitempty"` Status ToyStatus `json:"status,omitempty"` } ``` ## Subresources CRDs can choose to implement the `/status` and `/scale` [subresources][kube-subresources] as of Kubernetes 1.13. It's generally recommended that you make use of the `/status` subresource on all resources that have a status field. Both subresources have a corresponding [marker][crd-markers]. ### Status The status subresource is enabled via `+kubebuilder:subresource:status`. When enabled, updates at the main resource will not change status. Similarly, updates to the status subresource cannot change anything but the status field. For example: ```go // +kubebuilder:subresource:status type Toy struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec ToySpec `json:"spec,omitempty"` Status ToyStatus `json:"status,omitempty"` } ``` ### Scale The scale subresource is enabled via `+kubebuilder:subresource:scale`. When enabled, users will be able to use `kubectl scale` with your resource. If the `selectorpath` argument pointed to the string form of a label selector, the HorizontalPodAutoscaler will be able to autoscale your resource. For example: ```go type CustomSetSpec struct { Replicas *int32 `json:"replicas"` } type CustomSetStatus struct { Replicas int32 `json:"replicas"` Selector string `json:"selector"` // this must be the string form of the selector } // +kubebuilder:subresource:status // +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector type CustomSet struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec CustomSetSpec `json:"spec,omitempty"` Status CustomSetStatus `json:"status,omitempty"` } ``` ## Multiple Versions As of Kubernetes 1.13, you can have multiple versions of your Kind defined in your CRD, and use a webhook to convert between them. For more details on this process, see the [multiversion tutorial](/multiversion-tutorial/tutorial.md). By default, Kubebuilder disables generating different validation for different versions of the Kind in your CRD, to be compatible with older Kubernetes versions. You'll need to enable this by switching the line in your makefile that says `CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false` to `CRD_OPTIONS ?= crd:preserveUnknownFields=false` if using v1beta CRDs, and `CRD_OPTIONS ?= crd` if using v1 (recommended). Then, you can use the `+kubebuilder:storageversion` [marker][crd-markers] to indicate the [GVK](/cronjob-tutorial/gvks.md "Group-Version-Kind") that should be used to store data by the API server. ## Under the hood Kubebuilder scaffolds out make rules to run `controller-gen`. The rules will automatically install controller-gen if it's not on your path using `go install` with Go modules. You can also run `controller-gen` directly, if you want to see what it's doing. Each controller-gen "generator" is controlled by an option to controller-gen, using the same syntax as markers. controller-gen also supports different output "rules" to control how and where output goes. Notice the `manifests` make rule (condensed slightly to only generate CRDs): ```makefile # Generate manifests for CRDs manifests: controller-gen $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases ``` It uses the `output:crd:artifacts` output rule to indicate that CRD-related config (non-code) artifacts should end up in `config/crd/bases` instead of `config/crd`. To see all the options including generators for `controller-gen`, run ```shell $ controller-gen -h ``` or, for more details: ```shell $ controller-gen -hhh ``` [marker-ref]: ./markers.md "Markers for Config/Code Generation" [kube-validation]: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#validation "Custom Resource Definitions: Validation" [openapi-schema]: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject "OpenAPI v3" [kube-additional-printer-columns]: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#additional-printer-columns "Custom Resource Definitions: Additional Printer Columns" [kube-subresources]: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#status-subresource "Custom Resource Definitions: Status Subresource" [crd-markers]: ./markers/crd.md "CRD Generation" [controller-tools]: https://sigs.k8s.io/controller-tools "Controller Tools" ================================================ FILE: docs/book/src/reference/good-practices.md ================================================ # Good Practices ## What is "Reconciliation" in Operators? When you create a project using Kubebuilder, see the scaffolded code generated under `cmd/main.go`. This code initializes a [Manager][controller-runtime-manager], and the project relies on the [controller-runtime][controller-runtime] framework. The Manager manages [Controllers][controllers], which offer a reconcile function that synchronizes resources until the desired state is achieved within the cluster. Reconciliation is an ongoing loop that executes necessary operations to maintain the desired state, adhering to Kubernetes principles, such as the [control loop][k8s-control-loop]. For further information, check out the [Operator patterns][k8s-operator-pattern] documentation from Kubernetes to better understand those concepts. ## Why should reconciliations be idempotent? When developing operators, the controller’s reconciliation loop needs to be idempotent. By following the [Operator pattern][operator-pattern] we create [controllers][controllers] that provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. Developing idempotent solutions will allow the reconciler to correctly respond to generic or unexpected events, easily deal with application startup or upgrade. More explanation on this is available [here][controller-runtime-topic]. Writing reconciliation logic according to specific events, breaks the recommendation of operator pattern and goes against the design principles of [controller-runtime][controller-runtime]. This may lead to unforeseen consequences, such as resources becoming stuck and requiring manual intervention. ## Understanding Kubernetes APIs and following API conventions Building your operator commonly involves extending the Kubernetes API itself. It is helpful to understand precisely how Custom Resource Definitions (CRDs) interact with the Kubernetes API. Also, the [Kubebuilder documentation][docs] on Groups and Versions and Kinds may be helpful to understand these concepts better as they relate to operators. Additionally, we recommend checking the documentation on [Operator patterns][operator-pattern] from Kubernetes to better understand the purpose of the standard solutions built with KubeBuilder. ## Why you should adhere to the Kubernetes API conventions and standards Embracing the [Kubernetes API conventions and standards][k8s-api-conventions] is crucial for maximizing the potential of your applications and deployments. By adhering to these established practices, you can benefit in several ways. Firstly, adherence ensures seamless interoperability within the Kubernetes ecosystem. Following conventions allows your applications to work harmoniously with other components, reducing compatibility issues and promoting a consistent user experience. Secondly, sticking to API standards enhances the maintainability and troubleshooting of your applications. Adopting familiar patterns and structures makes debugging and supporting your deployments easier, leading to more efficient operations and quicker issue resolution. Furthermore, leveraging the Kubernetes API conventions empowers you to harness the platform's full capabilities. By working within the defined framework, you can leverage the rich set of features and resources offered by Kubernetes, enabling scalability, performance optimization, and resilience. Lastly, embracing these standards future-proofs your native solutions. By aligning with the evolving Kubernetes ecosystem, you ensure compatibility with future updates, new features, and enhancements introduced by the vibrant Kubernetes community. In summary, by adhering to the Kubernetes API conventions and standards, you unlock the potential for seamless integration, simplified maintenance, optimal performance, and future-readiness, all contributing to the success of your applications and deployments. ## Why should one avoid a system design where a single controller is responsible for managing multiple CRDs (Custom Resource Definitions)(for example, an _'install_all_controller.go'_)? Avoid a design solution where the same controller reconciles more than one Kind. Having many Kinds (such as CRDs), that are all managed by the same controller, usually goes against the design proposed by controller-runtime. Furthermore, this might hurt concepts such as encapsulation, the Single Responsibility Principle, and Cohesion. Damaging these concepts may cause unexpected side effects and increase the difficulty of extending, reusing, or maintaining the operator. Having one controller manage many Custom Resources (CRs) in an Operator can lead to several issues: - **Complexity**: A single controller managing multiple CRs can increase the complexity of the code, making it harder to understand, maintain, and debug. - **Scalability**: Each controller typically manages a single kind of CR for scalability. If a single controller handles multiple CRs, it could become a bottleneck, reducing the overall efficiency and responsiveness of your system. - **Single Responsibility Principle**: Following this principle from software engineering, each controller should ideally have only one job. This approach simplifies development and debugging, and makes the system more robust. - **Error Isolation**: If one controller manages multiple CRs and an error occurs, it could potentially impact all the CRs it manages. Having a single controller per CR ensures that an issue with one controller or CR does not directly affect others. - **Concurrency and Synchronization**: A single controller managing multiple CRs could lead to race conditions and require complex synchronization, especially if the CRs have interdependencies. In conclusion, while it might seem efficient to have a single controller manage multiple CRs, it often leads to higher complexity, lower scalability, and potential stability issues. It's generally better to adhere to the single responsibility principle, where each CR is managed by its own controller. ## Why You Should Adopt Status Conditions We recommend you manage your solutions using Status Conditionals following the [K8s Api conventions][k8s-api-conventions] because: - **Standardization**: Conditions provide a standardized way to represent the state of an Operator's custom resources, making it easier for users and tools to understand and interpret the resource's status. - **Readability**: Conditions can clearly express complex states by using a combination of multiple conditions, making it easier for users to understand the current state and progress of the resource. - **Extensibility**: As new features or states are added to your Operator, conditions can be easily extended to represent these new states without requiring significant changes to the existing API or structure. - **Observability**: Status conditions can be monitored and tracked by cluster administrators and external monitoring tools, enabling better visibility into the state of the custom resources managed by the Operator. - **Compatibility**: By adopting the common pattern of using conditions in Kubernetes APIs, Operator authors ensure their custom resources align with the broader ecosystem, which helps users to have a consistent experience when interacting with multiple Operators and resources in their clusters. ## You Should Adopt K8s Conventions for Instrumentation and Observability Proper logging is essential for observability in Kubernetes-native applications. However, it's important to understand which logging conventions to apply based on the context of your code. ### Understanding Go vs. Kubernetes Logging Conventions When developing with Go, you may be familiar with the [Go Code Review Comments][go-code-review] guidelines, which state that error strings should not be capitalized and should not end with punctuation. These conventions are designed for error messages that are often composed into larger contexts: ```go // Go conventions (for general Go code, libraries, CLI tools) return fmt.Errorf("something bad happened") // lowercase, no period log.Printf("failed to connect: %v", err) // lowercase ``` **However**, when developing Kubernetes-native solutions (controllers, operators, webhooks) that run on the cluster, you should follow the [Kubernetes Logging Conventions][k8s-logging] for better observability and consistency with the Kubernetes ecosystem. ### Kubernetes Logging Conventions For controllers, operators, and webhooks, follow these guidelines: - Start from a capital letter. - Do not end the message with a period. - Use active voice. Use complete sentences when there is an acting subject ("A could not do B") or omit the subject if the subject would be the program itself ("Could not do B"). - Use past tense ("Could not delete B" instead of "Cannot delete B") - When referring to an object, state what type of object it is. ("Deleted Pod" instead of "Deleted") - Use structured logging with balanced key-value pairs. **Examples:** ```go // Kubernetes conventions (for controllers, operators, webhooks) log.Info("Starting reconciliation") // Capital letter, no period log.Info("Creating Deployment", "name", name, "namespace", ns) // Specify object type, structured logging log.Info("Created Deployment", "name", deploy.Name) // Past tense, specify type log.Error(err, "Failed to create Pod", "name", name) // Past tense, specify type log.Info("Deployment could not create Pod", "deployment", name) // Acting subject log.Info("Could not delete Pod", "name", name) // Subject is the program itself ``` ### Why Different Conventions? - **Go conventions** are optimized for error messages that get composed into larger contexts and displayed inline with other text - **Kubernetes conventions** are optimized for structured logging in distributed systems where logs are: - Aggregated from multiple components across the cluster - Parsed by log collectors (Fluentd, Fluentbit, Loki, etc.) - Displayed in monitoring dashboards and UIs - Used for alerting and troubleshooting in production Following these conventions ensures your logs integrate seamlessly with Kubernetes observability tools and provide clear, actionable information for cluster operators and SREs. [docs]: /cronjob-tutorial/gvks.html [operator-pattern]: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ [controllers]: https://kubernetes.io/docs/concepts/architecture/controller/ [controller-runtime-topic]: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md#q-how-do-i-have-different-logic-in-my-reconciler-for-different-types-of-events-eg-create-update-delete [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [deploy-image]: /plugins/available/deploy-image-plugin-v1-alpha.md [controller-runtime-manager]: https://github.com/kubernetes-sigs/controller-runtime/blob/304027bcbe4b3f6d582180aec5759eb4db3f17fd/pkg/manager/manager.go#L53 [k8s-api-conventions]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md [k8s-control-loop]: https://kubernetes.io/docs/concepts/architecture/controller/ [k8s-operator-pattern]: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ [go-code-review]: https://go.dev/wiki/CodeReviewComments#error-strings [k8s-logging]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines ================================================ FILE: docs/book/src/reference/kind-config.yaml ================================================ kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane - role: worker - role: worker - role: worker ================================================ FILE: docs/book/src/reference/kind.md ================================================ # Using Kind For Development Purposes and CI ## Why Use Kind - **Fast Setup:** Launch a multi-node Kubernetes cluster locally in under a minute. - **Quick Teardown:** Dismantle the cluster in just a few seconds, streamlining your development workflow. - **Local Image Usage:** Deploy your container images directly without the need to push to a remote registry. - **Lightweight and Efficient:** Kind is a minimalistic Kubernetes distribution, making it perfect for local development and CI/CD pipelines. This only cover the basics to use a kind cluster. You can find more details at [kind documentation](https://kind.sigs.k8s.io/). ## Installation You can follow [this](https://kind.sigs.k8s.io/#installation-and-usage) to install `kind`. ## Create a Cluster You can simply create a `kind` cluster by ```bash kind create cluster ``` To customize your cluster, you can provide additional configuration. For example, the following is a sample `kind` configuration. ```yaml {{#include ./kind-config.yaml}} ``` Using the configuration above, run the following command will give you a k8s v1.17.2 cluster with 1 control-plane node and 3 worker nodes. ```bash kind create cluster --config hack/kind-config.yaml --image=kindest/node:v1.17.2 ``` You can use `--image` flag to specify the cluster version you want, e.g. `--image=kindest/node:v1.17.2`, the supported version are listed [here](https://hub.docker.com/r/kindest/node/tags). ## Load Docker Image into the Cluster When developing with a local kind cluster, loading docker images to the cluster is a very useful feature. You can avoid using a container registry. ```bash kind load docker-image your-image-name:your-tag ``` See [Load a local image into a kind cluster](https://kind.sigs.k8s.io/docs/user/quick-start/#loading-an-image-into-your-cluster) for more information. ## Delete a Cluster ```bash kind delete cluster ``` ================================================ FILE: docs/book/src/reference/manager-scope.md ================================================ # Manager Scope Manager scope determines which namespace(s) your manager watches and manages resources in. ## Overview Kubebuilder supports three types of manager scope: | Scope | Description | Use Case | |-------|-------------|----------| | **Cluster-scoped (default)** | Watches all namespaces in the cluster | Single manager managing resources cluster-wide | | **Namespace-scoped** | Watches only specific namespace(s) | Multi-tenant, least-privilege deployments | | **Multi-namespace** | Watches multiple specific namespaces | Manager managing resources in subset of namespaces | Manager scope is configured through: - RBAC resources (Role vs ClusterRole) - Cache configuration in `cmd/main.go` - `WATCH_NAMESPACE` environment variable ## Cluster-Scoped (Default) By default, Kubebuilder scaffolds cluster-scoped managers that watch all namespaces in the cluster. ```bash kubebuilder init --domain example.com ``` **Characteristics:** - Uses `ClusterRole` and `ClusterRoleBinding` for RBAC - Manager watches all namespaces - No cache configuration needed **When to use:** - Single manager instance for the entire cluster - Managing cluster-scoped resources (Nodes, ClusterRoles, Namespaces) - Simpler RBAC model when cluster-wide access is acceptable ## Namespace-Scoped Namespace-scoped managers watch only specific namespace(s), configured via the `WATCH_NAMESPACE` environment variable. ```bash # New projects kubebuilder init --domain example.com --namespaced # Existing projects kubebuilder edit --namespaced=true ``` **Characteristics:** - Uses namespace-scoped `Role` and `RoleBinding` for RBAC - Manager watches only specified namespace(s) - Requires cache configuration in `cmd/main.go` - Requires `namespace=` parameter in controller RBAC markers **When to use:** - Multi-tenant environments (one manager per tenant/namespace) - Security policies requiring least-privilege access - Multiple manager instances in different namespaces **RBAC markers:** Controllers in namespace-scoped projects use the `namespace=` parameter in RBAC markers to generate namespace-scoped `Role` resources: ```go // +kubebuilder:rbac:groups=myapp.example.com,namespace=myproject-system,resources=mykinds,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=myapp.example.com,namespace=myproject-system,resources=mykinds/status,verbs=get;update;patch // +kubebuilder:rbac:groups=myapp.example.com,namespace=myproject-system,resources=mykinds/finalizers,verbs=update ``` When controller-gen sees the `namespace=` parameter, it generates `kind: Role` instead of `kind: ClusterRole`. The namespace field is added by kustomize during the build process (configured in `config/default/kustomization.yaml`). **Cache configuration:** Kubebuilder automatically scaffolds the cache configuration in `cmd/main.go` when using `--namespaced` flag: ```go // setupCacheNamespaces configures the cache to watch specific namespace(s). // It supports both single namespace ("ns1") and multi-namespace ("ns1,ns2,ns3") formats. func setupCacheNamespaces(namespaces string) cache.Options { defaultNamespaces := make(map[string]cache.Config) for ns := range strings.SplitSeq(namespaces, ",") { defaultNamespaces[strings.TrimSpace(ns)] = cache.Config{} } return cache.Options{ DefaultNamespaces: defaultNamespaces, } } // In main() watchNamespace, err := getWatchNamespace() if err != nil { setupLog.Error(err, "Unable to get WATCH_NAMESPACE") os.Exit(1) } mgrOptions := ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, WebhookServer: webhookServer, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "your-leader-election-id", } // Configure cache to watch namespace(s) specified in WATCH_NAMESPACE mgrOptions.Cache = setupCacheNamespaces(watchNamespace) setupLog.Info("Watching namespace(s)", "namespaces", watchNamespace) mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions) ``` This configuration works for both single namespace (`WATCH_NAMESPACE=my-namespace`) and multi-namespace (`WATCH_NAMESPACE=ns1,ns2,ns3`) scenarios. ## Multi-Namespace Managers can watch multiple specific namespaces using comma-separated values in `WATCH_NAMESPACE`. **Characteristics:** - Requires `Role` and `RoleBinding` in each watched namespace - Uses the same `setupCacheNamespaces` helper function - Same code as single-namespace mode (KISS principle) **Example:** ```bash # Deploy manager to watch multiple namespaces export WATCH_NAMESPACE=namespace1,namespace2,namespace3 kubectl apply -f dist/install.yaml ``` The `setupCacheNamespaces` helper function automatically handles both single and multiple namespaces without conditional logic. ## See Also - [Understanding Scopes](./scopes.md) - Overview of manager and CRD scopes - [CRD Scope](./crd-scope.md) - Configuring CustomResourceDefinition scope - [Namespace-Scoped Migration](../migration/namespace-scoped.md) - Detailed implementation guide - [Project Config](./project-config.md) - PROJECT file configuration ================================================ FILE: docs/book/src/reference/markers/crd-processing.md ================================================ # CRD Processing These markers help control how the Kubernetes API server processes API requests involving your custom resources. See [Generating CRDs](/reference/generating-crd.md) for examples. {{#markerdocs CRD processing}} ================================================ FILE: docs/book/src/reference/markers/crd-validation.md ================================================ # CRD Validation These markers modify how the CRD validation schema is produced for the types and fields they modify. Each corresponds roughly to an OpenAPI/JSON schema option. See [Generating CRDs](/reference/generating-crd.md) for examples. {{#markerdocs CRD validation}} ================================================ FILE: docs/book/src/reference/markers/crd.md ================================================ # CRD Generation These markers describe how to construct a custom resource definition from a series of Go types and packages. Generation of the actual validation schema is described by the [validation markers](./crd-validation.md). See [Generating CRDs](../generating-crd.md) for examples. {{#markerdocs CRD}} ================================================ FILE: docs/book/src/reference/markers/object.md ================================================ # Object/DeepCopy These markers control when `DeepCopy` and `runtime.Object` implementation methods are generated. {{#markerdocs object}} ================================================ FILE: docs/book/src/reference/markers/rbac.md ================================================ # RBAC These markers cause an [RBAC ClusterRole](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole) to be generated. This allows you to describe the permissions that your controller requires alongside the code that makes use of those permissions. {{#markerdocs RBAC}} ================================================ FILE: docs/book/src/reference/markers/scaffold.md ================================================ # Scaffold The `+kubebuilder:scaffold` marker is a key part of the Kubebuilder scaffolding system. It marks locations in generated files where additional code will be injected as new resources (such as controllers, webhooks, or APIs) are scaffolded. This enables Kubebuilder to seamlessly integrate newly generated components into the project without affecting user-defined code. ## How It Works When you scaffold a new resource using the Kubebuilder CLI (e.g., `kubebuilder create api`), the CLI identifies `+kubebuilder:scaffold` markers in key locations and uses them as placeholders to insert the required imports and registration code. ## Example Usage in `main.go` Here is how the `+kubebuilder:scaffold` marker is used in a typical `main.go` file. To illustrate how it works, consider the following command to create a new API: ```shell kubebuilder create api --group crew --version v1 --kind Admiral --controller=true --resource=true ``` ### To Add New Imports The `+kubebuilder:scaffold:imports` marker allows the Kubebuilder CLI to inject additional imports, such as for new controllers or webhooks. When we create a new API, the CLI automatically adds the required import paths in this section. For example, after creating the `Admiral` API in a single-group layout, the CLI will add `crewv1 "/api/v1"` to the imports: ```go import ( "crypto/tls" "flag" "os" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" ... crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" // +kubebuilder:scaffold:imports ) ``` ### To Register a New Scheme The `+kubebuilder:scaffold:scheme` marker is used to register newly created API versions with the runtime scheme, ensuring the API types are recognized by the manager. For example, after creating the Admiral API, the CLI will inject the following code into the `init()` function to register the scheme: ```go func init() { ... utilruntime.Must(crewv1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } ``` ## To Set Up a Controller When we create a new controller (e.g., for Admiral), the Kubebuilder CLI injects the controller setup code into the manager using the `+kubebuilder:scaffold:builder` marker. This marker indicates where the setup code for new controllers should be added. For example, after creating the `AdmiralReconciler`, the CLI will add the following code to register the controller with the manager: ```go if err = (&crewv1.AdmiralReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Admiral") os.Exit(1) } // +kubebuilder:scaffold:builder ``` The `+kubebuilder:scaffold:builder` marker ensures that newly scaffolded controllers are properly registered with the manager, so that the controller can reconcile the resource. ## List of `+kubebuilder:scaffold` Markers | Marker | Usual Location | Function | |--------------------------------------------------------------------------------|------------------------------|---------------------------------------------------------------------------------| | `+kubebuilder:scaffold:imports` | `main.go` | Marks where imports for new controllers, webhooks, or APIs should be injected. | | `+kubebuilder:scaffold:scheme` | `init()` in `main.go` | Used to add API versions to the scheme for runtime. | | `+kubebuilder:scaffold:builder` | `main.go` | Marks where new controllers should be registered with the manager. | | `+kubebuilder:scaffold:webhook` | `webhooks suite tests` files | Marks where webhook setup functions are added. | | `+kubebuilder:scaffold:crdkustomizeresource` | `config/crd` | Marks where CRD custom resource patches are added. | | `+kubebuilder:scaffold:crdkustomizewebhookpatch` | `config/crd` | Marks where CRD webhook patches are added. | | `+kubebuilder:scaffold:crdkustomizecainjectionns` | `config/default` | Marks where CA injection patches are added for the conversion webhooks. | | `+kubebuilder:scaffold:crdkustomizecainjectioname` | `config/default` | Marks where CA injection patches are added for the conversion webhooks. | | **(No longer supported)** `+kubebuilder:scaffold:crdkustomizecainjectionpatch` | `config/crd` | Marks where CA injection patches are added for the webhooks. Replaced by `+kubebuilder:scaffold:crdkustomizecainjectionns` and `+kubebuilder:scaffold:crdkustomizecainjectioname` | | `+kubebuilder:scaffold:manifestskustomizesamples` | `config/samples` | Marks where Kustomize sample manifests are injected. | | `+kubebuilder:scaffold:e2e-webhooks-checks` | `test/e2e` | Adds e2e checks for webhooks depending on the types of webhooks scaffolded. | | `+kubebuilder:scaffold:e2e-metrics-webhooks-readiness` | `test/e2e` | Adds readiness logic so metrics e2e tests wait for webhook service endpoints before creating pods. | ================================================ FILE: docs/book/src/reference/markers/webhook.md ================================================ # Webhook These markers describe how [webhook configuration](../webhook-overview.md) is generated. Use these to keep the description of your webhooks close to the code that implements them. {{#markerdocs Webhook}} ================================================ FILE: docs/book/src/reference/markers.md ================================================ # Markers for Config/Code Generation Kubebuilder makes use of a tool called [controller-gen](/reference/controller-gen.md) for generating utility code and Kubernetes YAML. This code and config generation is controlled by the presence of special "marker comments" in Go code. Markers are single-line comments that start with a plus, followed by a marker name, optionally followed by some marker specific configuration: ```go // +kubebuilder:validation:Optional // +kubebuilder:validation:MaxItems=2 // +kubebuilder:printcolumn:JSONPath=".status.replicas",name=Replicas,type=string ``` See each subsection for information about different types of code and YAML generation. ## Generating Code & Artifacts in Kubebuilder Kubebuilder projects have two `make` targets that make use of controller-gen: - `make manifests` generates Kubernetes object YAML, like [CustomResourceDefinitions](./markers/crd.md), [WebhookConfigurations](./markers/webhook.md), and [RBAC roles](./markers/rbac.md). - `make generate` generates code, like [runtime.Object/DeepCopy implementations](./markers/object.md). See [Generating CRDs](./generating-crd.md) for a comprehensive overview. ## Marker Syntax Exact syntax is described in the [godocs for controller-tools](https://pkg.go.dev/sigs.k8s.io/controller-tools/pkg/markers?tab=doc). In general, markers may either be: - **Empty** (`+kubebuilder:validation:Optional`): empty markers are like boolean flags on the command line -- just specifying them enables some behavior. - **Anonymous** (`+kubebuilder:validation:MaxItems=2`): anonymous markers take a single value as their argument. - **Multi-option** (`+kubebuilder:printcolumn:JSONPath=".status.replicas",name=Replicas,type=string`): multi-option markers take one or more named arguments. The first argument is separated from the name by a colon, and latter arguments are comma-separated. Order of arguments doesn't matter. Some arguments may be optional. Marker arguments may be strings, ints, bools, slices, or maps thereof. Strings, ints, and bools follow their Go syntax: ```go // +kubebuilder:validation:ExclusiveMaximum=false // +kubebuilder:validation:Format="date-time" // +kubebuilder:validation:Maximum=42 ``` For convenience, in simple cases the quotes may be omitted from strings, although this is not encouraged for anything other than single-word strings: ```go // +kubebuilder:validation:Type=string ``` Slices may be specified either by surrounding them with curly braces and separating with commas: ```go // +kubebuilder:webhooks:Enum={"crackers, Gromit, we forgot the crackers!","not even wensleydale?"} ``` or, in simple cases, by separating with semicolons: ```go // +kubebuilder:validation:Enum=Wallace;Gromit;Chicken ``` Maps are specified with string keys and values of any type (effectively `map[string]interface{}`). A map is surrounded by curly braces (`{}`), each key and value is separated by a colon (`:`), and each key-value pair is separated by a comma: ```go // +kubebuilder:default={magic: {numero: 42, stringified: forty-two}} ``` ================================================ FILE: docs/book/src/reference/metrics-reference.md ================================================ # Default Exported Metrics References Following the metrics which are exported and provided by [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) by default: | Metrics name | Type | Description | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [workqueue_depth](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/workqueue.go#L41) | Gauge | Current depth of workqueue. | | [workqueue_adds_total](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/workqueue.go#L47) | Counter | Total number of adds handled by workqueue. | | [workqueue_queue_duration_seconds](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/workqueue.go#L53) | Histogram | How long in seconds an item stays in workqueue before being requested. | | [workqueue_work_duration_seconds](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/workqueue.go#L60) | Histogram | How long in seconds processing an item from workqueue takes. | | [workqueue_unfinished_work_seconds](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/workqueue.go#L67) | Gauge | How many seconds of work has been done that is in progress and hasn't been observed by work_duration. Large values indicate stuck threads. One can deduce the number of stuck threads by observing the rate at which this increases. | | [workqueue_longest_running_processor_seconds](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/workqueue.go#L76) | Gauge | How many seconds has the longest running processor for workqueue been running. | | [workqueue_retries_total](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/workqueue.go#L83) | Counter | Total number of retries handled by workqueue. | | [rest_client_requests_total ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/metrics/client_go_adapter.go#L33) | Counter | Number of HTTP requests, partitioned by status code, method, and host. | | [controller_runtime_reconcile_total ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/internal/controller/metrics/metrics.go#L30) | Counter | Total number of reconciliations per controller. | | [controller_runtime_reconcile_errors_total ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/internal/controller/metrics/metrics.go#L37) | Counter | Total number of reconciliation errors per controller. | | [controller_runtime_terminal_reconcile_errors_total ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/internal/controller/metrics/metrics.go#L44) | Counter | Total number of terminal errors from the reconciler. | | [controller_runtime_reconcile_time_seconds ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/internal/controller/metrics/metrics.go#L51) | Histogram | Length of time per reconciliation per controller. | | [controller_runtime_max_concurrent_reconciles ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/internal/controller/metrics/metrics.go#L60) | Gauge | Maximum number of concurrent reconciles per controller. | | [controller_runtime_active_workers ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/internal/controller/metrics/metrics.go#L67) | Gauge | Number of currently used workers per controller. | | [controller_runtime_webhook_latency_seconds ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/webhook/internal/metrics/metrics.go#L31) | Histogram | Histogram of the latency of processing admission requests. | | [controller_runtime_webhook_requests_total ](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/webhook/internal/metrics/metrics.go#L40) | Counter | Total number of admission requests by HTTP status code. | | [controller_runtime_webhook_requests_in_flight](https://github.com/kubernetes-sigs/controller-runtime/blob/v0.16.3/pkg/webhook/internal/metrics/metrics.go#L51) | Gauge | Current number of admission requests being served. | ================================================ FILE: docs/book/src/reference/metrics.md ================================================ # Metrics By default, controller-runtime builds a global prometheus registry and publishes [a collection of performance metrics](/reference/metrics-reference.md) for each controller. ## Metrics Configuration By looking at the file `config/default/kustomization.yaml` you can check the metrics are exposed by default: ```yaml # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml ``` ```yaml patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: kind: Deployment ``` Then, you can check in the `cmd/main.go` where metrics server is configured: ```go // Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server. // For more info: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/metrics/server Metrics: metricsserver.Options{ ... }, ``` ## Consuming Controller Metrics in Kubebuilder You can consume the metrics exposed by the controller using the `curl` command or any other HTTP client such as Prometheus. However, before doing so, ensure that your client has the **required RBAC permissions** to access the `/metrics` endpoint. ### Granting Permissions to Access Metrics Kubebuilder scaffolds a `ClusterRole` with the necessary read permissions under: ``` config/rbac/metrics_reader_role.yaml ``` This file contains the required RBAC rules to allow access to the metrics endpoint. #### Create a ClusterRoleBinding You can create the binding via `kubectl`: ```bash kubectl create clusterrolebinding metrics \ --clusterrole=-metrics-reader \ --serviceaccount=: ``` Or with a manifest: ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: allow-metrics-access roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: metrics-reader subjects: - kind: ServiceAccount name: controller-manager namespace: system # Replace 'system' with your controller-manager's namespace ``` ### Testing the Metrics Endpoint (via Curl Pod) If you'd like to manually test access to the metrics endpoint, follow these steps: - Create Role Binding ```bash kubectl create clusterrolebinding -metrics-binding \ --clusterrole=-metrics-reader \ --serviceaccount=-system:-controller-manager ``` - Generate a Token ```bash export TOKEN=$(kubectl create token -controller-manager -n -system) echo $TOKEN ``` - Launch Curl Pod ```bash kubectl run curl-metrics --rm -it --restart=Never \ --image=curlimages/curl:7.87.0 -n -system -- /bin/sh ``` - Call Metrics Endpoint Inside the pod, use: ```bash curl -v -k -H "Authorization: Bearer $TOKEN" \ https://-controller-manager-metrics-service.-system.svc.cluster.local:8443/metrics ``` ## Metrics Protection and available options Unprotected metrics endpoints can expose valuable data to unauthorized users, such as system performance, application behavior, and potentially confidential operational metrics. This exposure can lead to security vulnerabilities where an attacker could gain insights into the system's operation and exploit weaknesses. ### By using authn/authz (Enabled by default) To mitigate these risks, Kubebuilder projects utilize authentication (authn) and authorization (authz) to protect the metrics endpoint. This approach ensures that only authorized users and service accounts can access sensitive metrics data, enhancing the overall security of the system. In the past, the [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) was employed to provide this protection. However, its usage has been discontinued in recent versions. Since the release of `v4.1.0`, projects have had the metrics endpoint enabled and protected by default using the [WithAuthenticationAndAuthorization](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/metrics/server) feature provided by controller-runtime. Therefore, you will find the following configuration: - In the `cmd/main.go`: ```go if secureMetrics { ... metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } ``` This configuration leverages the FilterProvider to enforce authentication and authorization on the metrics endpoint. By using this method, you ensure that the endpoint is accessible only to those with the appropriate permissions. - In the `config/rbac/kustomization.yaml`: ```yaml # The following RBAC configurations are used to protect # the metrics endpoint with authn/authz. These configurations # ensure that only authorized users and service accounts # can access the metrics endpoint. - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml ``` In this way, only Pods using the `ServiceAccount` token are authorized to read the metrics endpoint. For example: ```yaml apiVersion: v1 kind: Pod metadata: name: metrics-consumer namespace: system spec: # Use the scaffolded service account name to allow authn/authz serviceAccountName: controller-manager containers: - name: metrics-consumer image: curlimages/curl:latest command: ["/bin/sh"] args: - "-c" - > while true; do # Note here that we are passing the token obtained from the ServiceAccount to curl the metrics endpoint curl -s -k -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" https://controller-manager-metrics-service.system.svc.cluster.local:8443/metrics; sleep 60; done ``` ### **(Recommended)** Enabling certificates for Production (Disabled by default) Projects built with Kubebuilder releases `4.4.0` and above have the logic scaffolded to enable the usage of certificates managed by [CertManager](https://cert-manager.io/) for securing the metrics server. Following the steps below, you can configure your project to use certificates managed by CertManager. 1. **Enable Cert-Manager in `config/default/kustomization.yaml`:** - Uncomment the cert-manager resource to include it in your project: ```yaml - ../certmanager ``` 2. **Enable the Patch to configure the usage of the certs in the Controller Deployment in `config/default/kustomization.yaml`:** - Uncomment the `cert_metrics_manager_patch.yaml` to mount the `serving-cert` secret in the Manager Deployment. ```yaml # Uncomment the patches line if you enable Metrics and CertManager # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. # This patch will protect the metrics with certManager self-signed certs. - path: cert_metrics_manager_patch.yaml target: kind: Deployment ``` 3. **Enable the CertManager replaces for the Metrics Server certificates in `config/default/kustomization.yaml`:** - Uncomment the replacements block bellow. It is required to properly set the DNS names for the certificates configured under `config/certmanager`. ```yaml # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations #replacements: # - source: # Uncomment the following block to enable certificates for metrics # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.name # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 0 # create: true # # - source: # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.namespace # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 1 # create: true # ``` 4. **Enable the Patch for the `ServiceMonitor` to Use the Cert-Manager-Managed Secret `config/prometheus/kustomization.yaml`:** - Add or uncomment the `ServiceMonitor` patch to securely reference the cert-manager-managed secret, replacing insecure configurations with secure certificate verification: ```yaml # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus # to securely reference certificates created and managed by cert-manager. # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml # to mount the "metrics-server-cert" secret in the Manager Deployment. patches: - path: monitor_tls_patch.yaml target: kind: ServiceMonitor ``` > **NOTE** that the `ServiceMonitor` patch above will ensure that if you enable the Prometheus integration, it will securely reference the certificates created and managed by CertManager. But it will **not** enable the integration with Prometheus. To enable the integration with Prometheus, you need uncomment the `#- ../certmanager` in the `config/default/kustomization.yaml`. For more information, see [Exporting Metrics for Prometheus](#exporting-metrics-for-prometheus). ### **(Optional)** By using Network Policy (Disabled by default) NetworkPolicy acts as a basic firewall for pods within a Kubernetes cluster, controlling traffic flow at the IP address or port level. However, it doesn't handle `authn/authz`. Uncomment the following line in the `config/default/kustomization.yaml`: ``` # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. # Only CR(s) which uses webhooks and applied on namespaces labeled 'webhooks: enabled' will be able to work properly. #- ../network-policy ``` ## Exporting Metrics for Prometheus Follow the steps below to export the metrics using the Prometheus Operator: 1. Install Prometheus and Prometheus Operator. We recommend using [kube-prometheus](https://github.com/coreos/kube-prometheus#installing) in production if you don't have your own monitoring system. If you are just experimenting, you can only install Prometheus and Prometheus Operator. 2. Uncomment the line `- ../prometheus` in the `config/default/kustomization.yaml`. It creates the `ServiceMonitor` resource which enables exporting the metrics. ```yaml # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. - ../prometheus ``` Note that, when you install your project in the cluster, it will create the `ServiceMonitor` to export the metrics. To check the ServiceMonitor, run `kubectl get ServiceMonitor -n -system`. See an example: ``` $ kubectl get ServiceMonitor -n monitor-system NAME AGE monitor-controller-manager-metrics-monitor 2m8s ``` Also, notice that the metrics are exported by default through port `8443`. In this way, you are able to check the Prometheus metrics in its dashboard. To verify it, search for the metrics exported from the namespace where the project is running `{namespace="-system"}`. See an example: Screenshot 2019-10-02 at 13 07 13 ## Publishing Additional Metrics If you wish to publish additional metrics from your controllers, this can be easily achieved by using the global registry from `controller-runtime/pkg/metrics`. One way to achieve this is to declare your collectors as global variables and then register them using `init()` in the controller's package. For example: ```go import ( "github.com/prometheus/client_golang/prometheus" "sigs.k8s.io/controller-runtime/pkg/metrics" ) var ( goobers = prometheus.NewCounter( prometheus.CounterOpts{ Name: "goobers_total", Help: "Number of goobers processed", }, ) gooberFailures = prometheus.NewCounter( prometheus.CounterOpts{ Name: "goober_failures_total", Help: "Number of failed goobers", }, ) ) func init() { // Register custom metrics with the global prometheus registry metrics.Registry.MustRegister(goobers, gooberFailures) } ``` You may then record metrics to those collectors from any part of your reconcile loop. These metrics can be evaluated from anywhere in the operator code. Those metrics will be available for prometheus or other openmetrics systems to scrape. ![Screen Shot 2021-06-14 at 10 15 59 AM](https://user-images.githubusercontent.com/37827279/121932262-8843cd80-ccf9-11eb-9c8e-98d0eda80169.png) ================================================ FILE: docs/book/src/reference/platform.md ================================================ # Platforms Supported Kubebuilder produces solutions that by default can work on multiple platforms or specific ones, depending on how you build and configure your workloads. This guide aims to help you properly configure your projects according to your needs. ## Overview To provide support on specific or multiple platforms, you must ensure that all images used in workloads are built to support the desired platforms. Note that they may not be the same as the platform where you develop your solutions and use KubeBuilder, but instead the platform(s) where your solution should run and be distributed. It is recommended to build solutions that work on multiple platforms so that your project works on any Kubernetes cluster regardless of the underlying operating system and architecture. ## How to define which platforms are supported The following covers what you need to do to provide the support for one or more platforms or architectures. ### 1) Build workload images to provide the support for other platform(s) The images used in workloads such as in your Pods/Deployments will need to provide the support for this other platform. You can inspect the images using a ManifestList of supported platforms using the command [`docker manifest inspect `][docker-manifest], i.e.: ```shell $ docker manifest inspect myregistry/example/myimage:v0.0.1 { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:a274a1a2af811a1daf3fd6b48ff3d08feb757c2c3f3e98c59c7f85e550a99a32", "platform": { "architecture": "arm64", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:d801c41875f12ffd8211fffef2b3a3d1a301d99f149488d31f245676fa8bc5d9", "platform": { "architecture": "amd64", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:f4423c8667edb5372fb0eafb6ec599bae8212e75b87f67da3286f0291b4c8732", "platform": { "architecture": "s390x", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:621288f6573c012d7cf6642f6d9ab20dbaa35de3be6ac2c7a718257ec3aff333", "platform": { "architecture": "ppc64le", "os": "linux" } }, ] } ``` ### 2) (Recommended as a Best Practice) Ensure that node affinity expressions are set to match the supported platforms Kubernetes provides a mechanism called [nodeAffinity][node-affinity] which can be used to limit the possible node targets where a pod can be scheduled. This is especially important to ensure correct scheduling behavior in clusters with nodes that span across multiple platforms (i.e. heterogeneous clusters). **Kubernetes manifest example** ```yaml affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/arch operator: In values: - amd64 - arm64 - ppc64le - s390x - key: kubernetes.io/os operator: In values: - linux ``` **Golang Example** ```go Template: corev1.PodTemplateSpec{ ... Spec: corev1.PodSpec{ Affinity: &corev1.Affinity{ NodeAffinity: &corev1.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{ { MatchExpressions: []corev1.NodeSelectorRequirement{ { Key: "kubernetes.io/arch", Operator: "In", Values: []string{"amd64"}, }, { Key: "kubernetes.io/os", Operator: "In", Values: []string{"linux"}, }, }, }, }, }, }, }, SecurityContext: &corev1.PodSecurityContext{ ... }, Containers: []corev1.Container{{ ... }}, }, ``` ## Producing projects that support multiple platforms You can use [`docker buildx`][buildx] to cross-compile via emulation ([QEMU](https://www.qemu.org/)) to build the manager image. See that projects scaffold with the latest versions of Kubebuilder have the Makefile target `docker-buildx`. **Example of Usage** ```shell $ make docker-buildx IMG=myregistry/myoperator:v0.0.1 ``` Note that you need to ensure that all images and workloads required and used by your project will provide the same support as recommended above, and that you properly configure the [nodeAffinity][node-affinity] for all your workloads. Therefore, ensure that you uncomment the following code in the `config/manager/manager.yaml` file ```yaml # TODO(user): Uncomment the following code to configure the nodeAffinity expression # according to the platforms which are supported by your solution. # It is considered best practice to support multiple architectures. You can # build your manager image using the makefile target docker-buildx. # affinity: # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: kubernetes.io/arch # operator: In # values: # - amd64 # - arm64 # - ppc64le # - s390x # - key: kubernetes.io/os # operator: In # values: # - linux ``` ## Which (workload) images are created by default? Projects created with the Kubebuilder CLI have two workloads which are: ### Manager The container to run the manager implementation is configured in the `config/manager/manager.yaml` file. This image is built with the Dockerfile file scaffolded by default and contains the binary of the project \ which will be built via the command `go build -a -o manager main.go`. Note that when you run `make docker-build` OR `make docker-build IMG=myregistry/myprojectname:` an image will be built from the client host (local environment) and produce an image for the client os/arch, which is commonly linux/amd64 or linux/arm64. [node-affinity]: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity [docker-manifest]: https://docs.docker.com/engine/reference/commandline/manifest/ [buildx]: https://docs.docker.com/build/buildx/ [goreleaser-buildx]: https://goreleaser.com/customization/docker/#use-a-specific-builder-with-docker-buildx ================================================ FILE: docs/book/src/reference/pprof-tutorial.md ================================================ # Monitoring Performance with Pprof [Pprof][github], a Go profiling tool, helps identify performance bottlenecks in areas like CPU and memory usage. It's integrated with the controller-runtime library's HTTP server, enabling profiling via HTTP endpoints. You can visualize the data using go tool pprof. Since [Pprof][github] is built into controller-runtime, no separate installation is needed. [Manager options][manager-options-doc] make it easy to enable pprof and gather runtime metrics to optimize controller performance. ## How to use Pprof? 1. **Enabling Pprof** In your `cmd/main.go` file, add the field: ```golang mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ ... // PprofBindAddress is the TCP address that the controller should bind to // for serving pprof. Specify the manager address and the port that should be bind. PprofBindAddress: ":8082", ... }) ``` 2. **Test It Out** After enabling [Pprof][github], you need to build and deploy your controller to test it out. Follow the steps in the [Quick Start guide][quick-start-run-it] to run your project locally or on a cluster. Then, you can apply your CRs/samples in order to monitor the performance of its controllers. 3. **Exporting the data** Using `curl`, export the profiling statistics to a file like this: ```bash # Note that we are using the bind host and port configured via the # Manager Options in the cmd/main.go curl -s "http://127.0.0.1:8082/debug/pprof/profile" > ./cpu-profile.out ``` 4. **Visualizing the results on Browser** ```bash # Go tool will open a session on port 8080. # You can change this as per your own need. go tool pprof -http=:8080 ./cpu-profile.out ``` Visualization results will vary depending on the deployed workload, and the Controller's behavior. However, you'll see the result on your browser similar to this one: ![pprof-result-visualization](./images/pprof-result-visualization.png) [manager-options-doc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/manager [quick-start-run-it]: ../quick-start.md#test-it-out [github]: https://github.com/google/pprof ================================================ FILE: docs/book/src/reference/project-config.md ================================================ # Project Config ## Overview The Project Config represents the configuration of a KubeBuilder project. All projects that are scaffolded with the CLI (KB version 3.0 and higher) will generate the `PROJECT` file in the projects' root directory. Therefore, it will store all plugins and input data used to generate the project and APIs to better enable plugins to make useful decisions when scaffolding. ## Example Following is an example of a PROJECT config file which is the result of a project generated with two APIs using the [Deploy Image Plugin][deploy-image-plugin]. ```yaml # Code generated by tool. DO NOT EDIT. # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html domain: testproject.org cliVersion: v4.6.0 layout: - go.kubebuilder.io/v4 plugins: deploy-image.go.kubebuilder.io/v1-alpha: resources: - domain: testproject.org group: example.com kind: Memcached options: containerCommand: memcached,--memory-limit=64,-o,modern,-v containerPort: "11211" image: memcached:1.4.36-alpine runAsUser: "1001" version: v1alpha1 - domain: testproject.org group: example.com kind: Busybox options: image: busybox:1.28 version: v1alpha1 projectName: project-v4-with-deploy-image repo: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image resources: - api: crdVersion: v1 namespaced: true controller: true domain: testproject.org group: example.com kind: Memcached path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1 version: v1alpha1 webhooks: validation: true webhookVersion: v1 - api: crdVersion: v1 namespaced: true controller: true domain: testproject.org group: example.com kind: Busybox path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1 version: v1alpha1 - controller: true domain: io external: true group: cert-manager kind: Certificate path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 module: github.com/cert-manager/cert-manager@v1.18.2 version: v1 version: "3" ``` ## Why do we need to store the plugins and data used? Following some examples of motivations to track the input used: - check if a plugin can or cannot be scaffolded on top of an existing plugin (i.e.) plugin compatibility while chaining multiple of them together. - what operations can or cannot be done such as verify if the layout allow API(s) for different groups to be scaffolded for the current configuration or not. - verify what data can or not be used in the CLI operations such as to ensure that WebHooks can only be created for pre-existent API(s) Note that KubeBuilder is not only a CLI tool but can also be used as a library to allow users to create their plugins/tools, provide helpers and customizations on top of their existing projects - an example of which is [Operator-SDK][operator-sdk]. SDK leverages KubeBuilder to create plugins to allow users to work with other languages and provide helpers for their users to integrate their projects with, for example, the [Operator Framework solutions/OLM][olm]. You can check the [plugin's documentation][plugins-doc] to know more about creating custom plugins. Additionally, another motivation for the PROJECT file is to help us to create a feature that allows users to easily upgrade their projects by providing helpers that automatically re-scaffold the project. By having all the required metadata regarding the APIs, their configurations and versions in the PROJECT file. For example, it can be used to automate the process of re-scaffolding while migrating between plugin versions. ([More info][doc-design-helper]). ## Versioning The Project config is versioned according to its layout. For further information see [Versioning][versioning]. ## Layout Definition The `PROJECT` version `3` layout looks like: ```yaml domain: testproject.org cliVersion: v4.6.0 layout: - go.kubebuilder.io/v4 plugins: deploy-image.go.kubebuilder.io/v1-alpha: resources: - domain: testproject.org group: example.com kind: Memcached options: containerCommand: memcached,--memory-limit=64,-o,modern,-v containerPort: "11211" image: memcached:memcached:1.6.26-alpine3.19 runAsUser: "1001" version: v1alpha1 - domain: testproject.org group: example.com kind: Busybox options: image: busybox:1.36.1 version: v1alpha1 projectName: project-v4-with-deploy-image repo: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image resources: - api: crdVersion: v1 namespaced: true controller: true domain: testproject.org group: example.com kind: Memcached path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1 version: v1alpha1 webhooks: validation: true webhookVersion: v1 - api: crdVersion: v1 namespaced: true controller: true domain: testproject.org group: example.com kind: Busybox path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1 version: v1alpha1 - controller: true domain: io external: true group: cert-manager kind: Certificate path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 module: github.com/cert-manager/cert-manager@v1.18.2 version: v1 version: "3" ``` Now let's check its layout fields definition: | Field | Description | |-------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `cliVersion` | Used to record the specific CLI version used during project scaffolding with `init`. Helps identifying the version of the tooling employed, aiding in troubleshooting and ensuring compatibility with updates. | | `layout` | Defines the global plugins, e.g. a project `init` with `--plugins="go/v4,deploy-image/v1-alpha"` means that any sub-command used will always call its implementation for both plugins in a chain. | | `domain` | Store the domain of the project. This information can be provided by the user when the project is generate with the `init` sub-command and the `domain` flag. | | `plugins` | Defines the plugins used to do custom scaffolding, e.g. to use the optional `deploy-image/v1-alpha` plugin to do scaffolding for just a specific api via the command `kubebuider create api [options] --plugins=deploy-image/v1-alpha`. | | `projectName` | The name of the project. This will be used to scaffold the manager data. By default it is the name of the project directory, however, it can be provided by the user in the `init` sub-command via the `--project-name` flag. | | `repo` | The project repository which is the Golang module, e.g `github.com/example/myproject-operator`. | | `multigroup` | **(Optional)** When set to `true`, enables multi-group project layout. APIs are organized into group-specific directories (`api///`). Can be set during initialization via `kubebuilder init --multigroup` or enabled/disabled later via `kubebuilder edit --multigroup`. Default is `false` (omitted from PROJECT file). | | `namespaced` | **(Optional)** When set to `true`, configures the project for namespace-scoped deployment. The operator will only watch and manage resources within its deployment namespace, using namespace-scoped RBAC (`Role`/`RoleBinding` instead of `ClusterRole`/`ClusterRoleBinding`). Can be enabled/disabled via `kubebuilder edit --namespaced`. Default is `false` (cluster-scoped, omitted from PROJECT file). | | `resources` | An array of all resources which were scaffolded in the project. | | `resources.api` | The API scaffolded in the project via the sub-command `create api`. | | `resources.api.crdVersion` | The Kubernetes API version (`apiVersion`) used to do the scaffolding for the CRD resource. | | `resources.api.namespaced` | The API RBAC permissions which can be namespaced or cluster scoped. | | `resources.controller` | Indicates whether a controller was scaffolded for the API. | | `resources.domain` | The domain of the resource which was provided by the `--domain` flag when the project was initialized or via the flag `--external-api-domain` when it was used to scaffold controllers for an [External Type][external-type]. | | `resources.group` | The GKV group of the resource which is provided by the `--group` flag when the sub-command `create api` is used. | | `resources.version` | The GKV version of the resource which is provided by the `--version` flag when the sub-command `create api` is used. | | `resources.kind` | Store GKV Kind of the resource which is provided by the `--kind` flag when the sub-command `create api` is used. | | `resources.path` | The import path for the API resource. It will be `/api/` unless the API added to the project is an external or core-type. For the core-types scenarios, the paths used are mapped [here][core-types]. Or either the path informed by the flag `--external-api-path` | | `resources.core` | It is `true` when the group used is from Kubernetes API and the API resource is not defined on the project. | | `resources.external` | It is `true` when the flag `--external-api-path` was used to generated the scaffold for an [External Type][external-type]. | | `resources.module` | **(Optional)** The Go module path for external API dependencies, optionally including a version (e.g., `github.com/cert-manager/cert-manager@v1.18.2` or just `github.com/cert-manager/cert-manager`). Only used when `external` is `true`. Provided via the `--external-api-module` flag to explicitly pin a specific version in `go.mod` or to specify the module when it cannot be automatically determined from `--external-api-path`. If not provided, `go mod tidy` will resolve the dependency automatically. | | `resources.webhooks` | Store the webhooks data when the sub-command `create webhook` is used. | | `resources.webhooks.spoke` | Store the API version that will act as the Spoke with the designated Hub version for conversion webhooks. | | `resources.webhooks.webhookVersion` | The Kubernetes API version (`apiVersion`) used to scaffold the webhook resource. | | `resources.webhooks.conversion` | It is `true` when the webhook was scaffold with the `--conversion` flag which means that is a conversion webhook. | | `resources.webhooks.defaulting` | It is `true` when the webhook was scaffold with the `--defaulting` flag which means that is a defaulting webhook. | | `resources.webhooks.validation` | It is `true` when the webhook was scaffold with the `--programmatic-validation` flag which means that is a validation webhook. | [project]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/testdata/project-v3/PROJECT [versioning]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/VERSIONING.md#Versioning [core-types]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/pkg/plugins/golang/options.go [deploy-image-plugin]: ../plugins/available/deploy-image-plugin-v1-alpha.md [olm]: https://olm.operatorframework.io/ [plugins-doc]: ../plugins/creating-plugins.html#why-use-the-kubebuilder-style [doc-design-helper]: https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/helper_to_upgrade_projects_by_rescaffolding.md [operator-sdk]: https://sdk.operatorframework.io/ [external-type]: ./using_an_external_resource.md ================================================ FILE: docs/book/src/reference/raising-events.md ================================================ # Creating Events It is often useful to publish *Event* objects from the controller Reconcile function as they allow users or any automated processes to see what is going on with a particular object and respond to them. Recent Events for an object can be viewed by running `$ kubectl describe `. Also, they can be checked by running `$ kubectl get events`. ## Writing Events Anatomy of an Event: ```go Eventf(regarding, related runtime.Object, eventtype, reason, action, message string, args ...interface{}) ``` - `regarding` is the object this event is about. - `related` is an optional secondary object related to this event (use `nil` if not applicable). - `eventtype` is this event type, and is either *Normal* or *Warning*. ([More info][Event-Example]) - `reason` is the reason this event is generated. It should be short and unique with `UpperCamelCase` format. The value could appear in *switch* statements by automation. ([More info][Reason-Example]) - `action` is the action that was taken/failed regarding the object. - `message` is a human-readable description with optional format arguments. ### How to be able to raise Events? Following are the steps with examples to help you raise events in your controller's reconciliations. Events are published from a Controller using an [EventRecorder][Events]`type CorrelatorOptions struct`, which can be created for a Controller by calling `GetEventRecorder(name string)` on a Manager. See that we will change the implementation scaffolded in `cmd/main.go`: ```go if err := (&controller.MyKindReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), // Note that we added the following line: Recorder: mgr.GetEventRecorder("mykind-controller"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MyKind") os.Exit(1) } ``` ### Allowing usage of EventRecorder on the Controller To raise an event, you must have access to `events.EventRecorder` in the Controller. Therefore, firstly let's update the controller implementation: ```go import ( ... "k8s.io/client-go/tools/events" ... ) // MyKindReconciler reconciles a MyKind object type MyKindReconciler struct { client.Client Scheme *runtime.Scheme // See that we added the following code to allow us to pass the events.EventRecorder Recorder events.EventRecorder } ``` ### Passing the EventRecorder to the Controller Events are published from a Controller using an [EventRecorder]`type CorrelatorOptions struct`, which can be created for a Controller by calling `GetEventRecorder(name string)` on a Manager. See that we will change the implementation scaffolded in `cmd/main.go`: ```go if err := (&controller.MyKindReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), // Note that we added the following line: Recorder: mgr.GetEventRecorder("mykind-controller"), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "MyKind") os.Exit(1) } ``` ### Granting the required permissions You must also grant the RBAC rules permissions to allow your project to create Events. Therefore, ensure that you add the [RBAC][rbac-markers] into your controller: ```go ... // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch ... func (r *MyKindReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { ``` And then, run `$ make manifests` to update the rules under `config/rbac/role.yaml`. [Events]: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#events [Event-Example]: https://github.com/kubernetes/api/blob/6c11c9e4685cc62e4ddc8d4aaa824c46150c9148/core/v1/types.go#L6019-L6024 [Reason-Example]: https://github.com/kubernetes/api/blob/6c11c9e4685cc62e4ddc8d4aaa824c46150c9148/core/v1/types.go#L6048 [Message-Example]: https://github.com/kubernetes/api/blob/6c11c9e4685cc62e4ddc8d4aaa824c46150c9148/core/v1/types.go#L6053 [rbac-markers]: ./markers/rbac.md ================================================ FILE: docs/book/src/reference/reference.md ================================================ # Reference - [Generating CRDs](generating-crd.md) - [Using Finalizers](using-finalizers.md) Finalizers are a mechanism to execute any custom logic related to a resource before it gets deleted from Kubernetes cluster. - [Watching Resources](watching-resources.md) Watch resources in the Kubernetes cluster to be informed and take actions on changes. - [Watching Secondary Resources that are `Owned` ](watching-resources/secondary-owned-resources.md) - [Watching Secondary Resources that are NOT `Owned`](watching-resources/secondary-resources-not-owned) - [Using Predicates to Refine Watches](watching-resources/predicates-with-watch.md) - [Kind cluster](kind.md) - [What's a webhook?](webhook-overview.md) Webhooks are HTTP callbacks, there are 3 types of webhooks in k8s: 1) admission webhook 2) CRD conversion webhook 3) authorization webhook - [Admission webhook](admission-webhook.md) Admission webhooks are HTTP callbacks for mutating or validating resources before the API server admit them. - [Markers for Config/Code Generation](markers.md) - [CRD Generation](markers/crd.md) - [CRD Validation](markers/crd-validation.md) - [Webhook](markers/webhook.md) - [Object/DeepCopy](markers/object.md) - [RBAC](markers/rbac.md) - [Scaffold](markers/scaffold.md) - [Monitoring with Pprof](pprof-tutorial.md) - [controller-gen CLI](controller-gen.md) - [completion](completion.md) - [Artifacts](artifacts.md) - [Platform Support](platform.md) - [Sub-Module Layouts](submodule-layouts.md) - [Using an external Resource / API](using_an_external_resource.md) - [Metrics](metrics.md) - [Reference](metrics-reference.md) - [CLI plugins](../plugins/plugins.md) ================================================ FILE: docs/book/src/reference/scopes.md ================================================ # Understanding Scopes in Kubebuilder In Kubernetes, **scope** defines the boundaries within which a resource or controller operates. When building with Kubebuilder, you work with two independent scoping concepts: 1. **[Manager Scope](./manager-scope.md)** - Determines which namespace(s) your manager watches and operates in 2. **[CRD Scope](./crd-scope.md)** - Determines whether your custom resources are namespace-specific or cluster-wide ## What is Scope? Scope defines the visibility and access boundaries in a Kubernetes cluster: - **Cluster-scoped**: Operates across the entire cluster with access to all namespaces - **Namespace-scoped**: Limited to specific namespace(s) for isolation and security ## Manager Scope vs CRD Scope These concepts are **independent** and configured separately: - **Manager Scope**: Controls which namespace(s) the manager watches (configured via deployment RBAC and cache) - **CRD Scope**: Controls whether custom resources are namespace-specific or cluster-wide (configured in CRD manifest) You can combine them in different ways - for example, a cluster-scoped manager can manage namespace-scoped CRDs (the default pattern). ## Learn More For detailed information, configuration steps, and code examples: - **[Manager Scope](./manager-scope.md)** - Manager scope configuration, RBAC, cache setup, and namespace watching - **[CRD Scope](./crd-scope.md)** - CRD scope configuration, markers, and RBAC considerations - **[Migrating to Namespace-Scoped Manager](../migration/namespace-scoped.md)** - Step-by-step migration guide for existing projects ================================================ FILE: docs/book/src/reference/submodule-layouts.md ================================================ # Sub-Module Layouts This part describes how to modify a scaffolded project for use with multiple `go.mod` files for APIs and Controllers. Sub-Module Layouts (in a way you could call them a special form of [Monorepo's][monorepo]) are a special use case and can help in scenarios that involve reuse of APIs without introducing indirect dependencies that should not be available in the project consuming the API externally. ## Overview Separate `go.mod` modules for APIs and Controllers can help for the following cases: - There is an enterprise version of an operator available that wants to reuse APIs from the Community Version - There are many (possibly external) modules depending on the API and you want to have a more strict separation of transitive dependencies - If you want to reduce impact of transitive dependencies on your API being included in other projects - If you are looking to separately manage the lifecycle of your API release process from your controller release process. - If you are looking to modularize your codebase without splitting your code between multiple repositories. They introduce however multiple caveats into typical projects which is one of the main factors that makes them hard to recommend in a generic use-case or plugin: - Multiple `go.mod` modules are not recommended as a go best practice and [multiple modules are mostly discouraged][multi-module-repositories] - There is always the possibility to extract your APIs into a new repository and arguably also have more control over the release process in a project spanning multiple repos relying on the same API types. - It requires at least one [replace directive][replace-directives] either through `go.work` which is at least 2 more files plus an environment variable for build environments without GO_WORK or through `go.mod` replace, which has to be manually dropped and added for every release. ## Adjusting your Project For a proper Sub-Module layout, we will use the generated APIs as a starting point. For the steps below, we will assume you created your project in your `GOPATH` with ```shell kubebuilder init ``` and created an API & controller with ```shell kubebuilder create api --group operator --version v1alpha1 --kind Sample --resource --controller --make ``` ### Creating a second module for your API Now that we have a base layout in place, we will enable you for multiple modules. 1. Navigate to `api/v1alpha1` 2. Run `go mod init` to create a new submodule 3. Run `go mod tidy` to resolve the dependencies Your api go.mod file could now look like this: ```go.mod module YOUR_GO_PATH/test-operator/api/v1alpha1 go 1.21.0 require ( k8s.io/apimachinery v0.28.4 sigs.k8s.io/controller-runtime v0.16.3 ) require ( github.com/go-logr/logr v1.2.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/text v0.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/klog/v2 v2.100.1 // indirect k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) ``` As you can see it only includes apimachinery and controller-runtime as dependencies and any dependencies you have declared in your controller are not taken over into the indirect imports. ### Using replace directives for development When trying to resolve your main module in the root folder of the operator, you will notice an error if you use a VCS path: ```shell go mod tidy go: finding module for package YOUR_GO_PATH/test-operator/api/v1alpha1 YOUR_GO_PATH/test-operator imports YOUR_GO_PATH/test-operator/api/v1alpha1: cannot find module providing package YOUR_GO_PATH/test-operator/api/v1alpha1: module YOUR_GO_PATH/test-operator/api/v1alpha1: git ls-remote -q origin in LOCALVCSPATH: exit status 128: remote: Repository not found. fatal: repository 'https://YOUR_GO_PATH/test-operator/' not found ``` The reason for this is that you may have not pushed your modules into the VCS yet and resolving the main module will fail as it can no longer directly access the API types as a package but only as a module. To solve this issue, we will have to tell the go tooling to properly `replace` the API module with a local reference to your path. You can do this with 2 different approaches: go modules and go workspaces. #### Using go modules For go modules, you will edit the main `go.mod` file of your project and issue a replace directive. You can do this by editing the `go.mod` with `` ```shell go mod edit -require YOUR_GO_PATH/test-operator/api/v1alpha1@v0.0.0 # Only if you didn't already resolve the module go mod edit -replace YOUR_GO_PATH/test-operator/api/v1alpha1@v0.0.0=./api/v1alpha1 go mod tidy ``` Note that we used the placeholder version `v0.0.0` of the API Module. In case you already released your API module once, you can use the real version as well. However this will only work if the API Module is already available in the VCS. #### Using go workspaces For go workspaces, you will not edit the `go.mod` files yourself, but rely on the workspace support in go. To initialize a workspace for your project, run `go work init` in the project root. Now let us include both modules in our workspace: ```shell go work use . # This includes the main module with the controller go work use api/v1alpha1 # This is the API submodule go work sync ``` This will lead to commands such as `go run` or `go build` to respect the workspace and make sure that local resolution is used. You will be able to work with this locally without having to build your module. When using `go.work` files, it is recommended to not commit them into the repository and add them to `.gitignore`. ```gitignore go.work go.work.sum ``` When releasing with a present `go.work` file, make sure to set the environment variable `GOWORK=off` (verifiable with `go env GOWORK`) to make sure the release process does not get impeded by a potentially committed `go.work` file. #### Adjusting the Dockerfile When building your controller image, kubebuilder by default is not able to work with multiple modules. You will have to manually add the new API module into the download of dependencies: ```dockerfile # Build the manager binary FROM docker.io/golang:1.20 as builder ARG TARGETOS ARG TARGETARCH WORKDIR /workspace # Copy the Go Modules manifests COPY go.mod go.mod COPY go.sum go.sum # Copy the Go Sub-Module manifests COPY api/v1alpha1/go.mod api/go.mod COPY api/v1alpha1/go.sum api/go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer RUN go mod download # Copy the go source COPY cmd/main.go cmd/main.go COPY api/ api/ COPY internal/controller/ internal/controller/ # Build # the GOARCH has not a default value to allow the binary be built according to the host where the command # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details FROM gcr.io/distroless/static:nonroot WORKDIR / COPY --from=builder /workspace/manager . USER 65532:65532 ENTRYPOINT ["/manager"] ``` ### Creating a new API and controller release Because you adjusted the default layout, before releasing your first version of your operator, make sure to [familiarize yourself with mono-repo/multi-module releases][multi-module-repositories] with multiple `go.mod` files in different subdirectories. Assuming a single API was created, the release process could look like this: ```sh git commit git tag v1.0.0 # this is your main module release git tag api/v1.0.0 # this is your api release go mod edit -require YOUR_GO_PATH/test-operator/api@v1.0.0 # now we depend on the api module in the main module go mod edit -dropreplace YOUR_GO_PATH/test-operator/api/v1alpha1 # this will drop the replace directive for local development in case you use go modules, meaning the sources from the VCS will be used instead of the ones in your monorepo checked out locally. git push origin main v1.0.0 api/v1.0.0 ``` After this, your modules will be available in VCS and you do not need a local replacement anymore. However if you're making local changes, make sure to adopt your behavior with `replace` directives accordingly. ### Reusing your extracted API module Whenever you want to reuse your API module with a separate kubebuilder, we will assume you follow the guide for [using an external Type](/reference/using_an_external_type.md). When you get to the step `Edit the API files` simply import the dependency with ```shell go get YOUR_GO_PATH/test-operator/api@v1.0.0 ``` and then use it as explained in the guide. [basic-project-doc]: ./../cronjob-tutorial/basic-project.md [monorepo]: https://en.wikipedia.org/wiki/Monorepo [replace-directives]: https://go.dev/ref/mod#go-mod-file-replace [multi-module-repositories]: https://github.com/golang/go/wiki/Modules#faqs--multi-module-repositories ================================================ FILE: docs/book/src/reference/using-finalizers.md ================================================ # Using Finalizers `Finalizers` allow controllers to implement asynchronous pre-delete hooks. Let's say you create an external resource (such as a storage bucket) for each object of your API type, and you want to delete the associated external resource on object's deletion from Kubernetes, you can use a finalizer to do that. You can read more about the finalizers in the [Kubernetes reference docs](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#finalizers). The section below demonstrates how to register and trigger pre-delete hooks in the `Reconcile` method of a controller. The key point to note is that a finalizer causes "delete" on the object to become an "update" to set deletion timestamp. Presence of deletion timestamp on the object indicates that it is being deleted. Otherwise, without finalizers, a delete shows up as a reconcile where the object is missing from the cache. Highlights: - If the object is not being deleted and does not have the finalizer registered, then add the finalizer and update the object in Kubernetes. - If object is being deleted and the finalizer is still present in finalizers list, then execute the pre-delete logic and remove the finalizer and update the object. - Ensure that the pre-delete logic is idempotent. {{#literatego ../cronjob-tutorial/testdata/finalizer_example.go}} ================================================ FILE: docs/book/src/reference/using_an_external_resource.md ================================================ # Using External Resources In some cases, your project may need to work with resources that aren't defined by your own APIs. These external resources fall into two main categories: - **Core Types**: API types defined by Kubernetes itself, such as `Pods`, `Services`, and `Deployments`. - **External Types**: API types defined in other projects, such as CRDs defined by another solution. ## Managing External Types ### Creating a Controller for External Types To create a controller for an external type without scaffolding a resource, use the `create api` command with the `--resource=false` option and specify the path to the external API type using the `--external-api-path` and `--external-api-domain` flag options. This generates a controller for types defined outside your project, such as CRDs managed by other Operators. The command looks like this: ```shell kubebuilder create api --group --version --kind --controller --resource=false --external-api-path= --external-api-domain= ``` - `--external-api-path`: Provide the Go import path where the external types are defined. - `--external-api-domain`: Provide the domain for the external types. This value will be used to generate RBAC permissions and create the QualifiedGroup, such as - `apiGroups: .` For example, if you're managing Certificates from Cert Manager: ```shell kubebuilder create api --group certmanager --version v1 --kind Certificate --controller=true --resource=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io ``` See the RBAC [markers][markers-rbac] generated for this: ```go // +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/status,verbs=get;update;patch // +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/finalizers,verbs=update ``` Also, the RBAC role: ```ymal - apiGroups: - cert-manager.io resources: - certificates verbs: - create - delete - get - list - patch - update - watch - apiGroups: - cert-manager.io resources: - certificates/finalizers verbs: - update - apiGroups: - cert-manager.io resources: - certificates/status verbs: - get - patch - update ``` This scaffolds a controller for the external type but skips creating new resource definitions since the type is defined in an external project. ### Creating a Webhook to Manage an External Type You can create webhooks for external types by providing the external API path, domain, and optionally the module: ```shell kubebuilder create webhook --group certmanager --version v1 --kind Issuer \ --defaulting --programmatic-validation \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=cert-manager.io ``` You can also pin the version using the `--external-api-module` flag: ```shell kubebuilder create webhook --group certmanager --version v1 --kind Issuer \ --defaulting --programmatic-validation \ --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \ --external-api-domain=cert-manager.io \ --external-api-module=github.com/cert-manager/cert-manager@v1.18.2 ``` ## Managing Core Types Core Kubernetes API types, such as `Pods`, `Services`, and `Deployments`, are predefined by Kubernetes. To create a controller for these core types without scaffolding the resource, use the Kubernetes group name described in the following table and specify the version and kind. | Group | K8s API Group | |---------------------------|------------------------------------| | admission | k8s.io/admission | | admissionregistration | k8s.io/admissionregistration | | apps | apps | | auditregistration | k8s.io/auditregistration | | apiextensions | k8s.io/apiextensions | | authentication | k8s.io/authentication | | authorization | k8s.io/authorization | | autoscaling | autoscaling | | batch | batch | | certificates | k8s.io/certificates | | coordination | k8s.io/coordination | | core | core | | events | k8s.io/events | | extensions | extensions | | imagepolicy | k8s.io/imagepolicy | | networking | k8s.io/networking | | node | k8s.io/node | | metrics | k8s.io/metrics | | policy | policy | | rbac.authorization | k8s.io/rbac.authorization | | scheduling | k8s.io/scheduling | | setting | k8s.io/setting | | storage | k8s.io/storage | The command to create a controller to manage `Pods` looks like this: ```shell kubebuilder create api --group core --version v1 --kind Pod --controller=true --resource=false ``` For instance, to create a controller to manage Deployment the command would be like: ```sh create api --group apps --version v1 --kind Deployment --controller=true --resource=false ``` See the RBAC [markers][markers-rbac] generated for this: ```go // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch // +kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update ``` Also, the RBAC for the above [markers][markers-rbac]: ```yaml - apiGroups: - apps resources: - deployments verbs: - create - delete - get - list - patch - update - watch - apiGroups: - apps resources: - deployments/finalizers verbs: - update - apiGroups: - apps resources: - deployments/status verbs: - get - patch - update ``` This scaffolds a controller for the Core type `corev1.Pod` but skips creating new resource definitions since the type is already defined in the Kubernetes API. ### Creating a Webhook to Manage a Core Type You will run the command with the Core Type data, just as you would for controllers. See an example: ```go kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation ``` [markers-rbac]: ./markers/rbac.md ================================================ FILE: docs/book/src/reference/watching-resources/predicates-with-watch.md ================================================ # Using Predicates to Refine Watches When working with controllers, it's often beneficial to use **Predicates** to filter events and control when the reconciliation loop should be triggered. [Predicates][predicates-doc] allow you to define conditions based on events (such as create, update, or delete) and resource fields (such as labels, annotations, or status fields). By using **[Predicates][predicates-doc]**, you can refine your controller’s behavior to respond only to specific changes in the resources it watches. This can be especially useful when you want to refine which changes in resources should trigger a reconciliation. By using predicates, you avoid unnecessary reconciliations and can ensure that the controller only reacts to relevant changes. ## When to Use Predicates **Predicates are useful when:** - You want to ignore certain changes, such as updates that don't impact the fields your controller is concerned with. - You want to trigger reconciliation only for resources with specific labels or annotations. - You want to watch external resources and react only to specific changes. ## Example: Using Predicates to Filter Update Events Let’s say that we only want our **`BackupBusybox`** controller to reconcile when certain fields of the **`Busybox`** resource change, for example, when the `spec.size` field changes, but we want to ignore all other changes (such as status updates). ### Defining a Predicate In the following example, we define a predicate that only allows reconciliation when there’s a meaningful update to the **`Busybox`** resource: ```go import ( "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/event" ) // Predicate to trigger reconciliation only on size changes in the Busybox spec updatePred := predicate.Funcs{ // Only allow updates when the spec.size of the Busybox resource changes UpdateFunc: func(e event.UpdateEvent) bool { oldObj := e.ObjectOld.(*examplecomv1alpha1.Busybox) newObj := e.ObjectNew.(*examplecomv1alpha1.Busybox) // Trigger reconciliation only if the spec.size field has changed return oldObj.Spec.Size != newObj.Spec.Size }, // Allow create events CreateFunc: func(e event.CreateEvent) bool { return true }, // Allow delete events DeleteFunc: func(e event.DeleteEvent) bool { return true }, // Allow generic events (e.g., external triggers) GenericFunc: func(e event.GenericEvent) bool { return true }, } ``` ### Explanation In this example: - The **`UpdateFunc`** returns `true` only if the **`spec.size`** field has changed between the old and new objects, meaning that all other changes in the `spec`, like annotations or other fields, will be ignored. - **`CreateFunc`**, **`DeleteFunc`**, and **`GenericFunc`** return `true`, meaning that create, delete, and generic events are still processed, allowing reconciliation to happen for these event types. This ensures that the controller reconciles only when the specific field **`spec.size`** is modified, while ignoring any other modifications in the `spec` that are irrelevant to your logic. ### Example: Using Predicates in `Watches` Now, we apply this predicate in the **`Watches()`** method of the **`BackupBusyboxReconciler`** to trigger reconciliation only for relevant events: ```go // SetupWithManager sets up the controller with the Manager. // The controller will watch both the BackupBusybox primary resource and the Busybox resource, using predicates. func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&examplecomv1alpha1.BackupBusybox{}). // Watch the primary resource (BackupBusybox) Watches( &examplecomv1alpha1.Busybox{}, // Watch the Busybox CR handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { return []reconcile.Request{ { NamespacedName: types.NamespacedName{ Name: "backupbusybox", // Reconcile the associated BackupBusybox resource Namespace: obj.GetNamespace(), // Use the namespace of the changed Busybox }, }, } }), builder.WithPredicates(updatePred), // Apply the predicate ). // Trigger reconciliation when the Busybox resource changes (if it meets predicate conditions) Complete(r) } ``` ### Explanation - **[`builder.WithPredicates(updatePred)`][predicates-doc]**: This method applies the predicate, ensuring that reconciliation only occurs when the **`spec.size`** field in **`Busybox`** changes. - **Other Events**: The controller will still trigger reconciliation on `Create`, `Delete`, and `Generic` events. [predicates-doc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/source#WithPredicates ================================================ FILE: docs/book/src/reference/watching-resources/secondary-owned-resources.md ================================================ # Watching Secondary Resources `Owned` by the Controller In Kubernetes controllers, it’s common to manage both **Primary Resources** and **Secondary Resources**. A **Primary Resource** is the main resource that the controller is responsible for, while **Secondary Resources** are created and managed by the controller to support the **Primary Resource**. In this section, we will explain how to manage **Secondary Resources** which are `Owned` by the controller. This example shows how to: - Set the [Owner Reference][cr-owner-ref-doc] between the primary resource (`Busybox`) and the secondary resource (`Deployment`) to ensure proper lifecycle management. - Configure the controller to `Watch` the secondary resource using `Owns()` in `SetupWithManager()`. See that `Deployment` is owned by the `Busybox` controller because it will be created and managed by it. ## Setting the Owner Reference To link the lifecycle of the secondary resource (`Deployment`) to the primary resource (`Busybox`), we need to set an [Owner Reference][cr-owner-ref-doc] on the secondary resource. This ensures that Kubernetes automatically handles cascading deletions: if the primary resource is deleted, the secondary resource will also be deleted. Controller-runtime provides the [controllerutil.SetControllerReference][cr-owner-ref-doc] function, which you can use to set this relationship between the resources. ### Setting the Owner Reference Below, we create the `Deployment` and set the Owner reference between the `Busybox` custom resource and the `Deployment` using `controllerutil.SetControllerReference()`. ```go // deploymentForBusybox returns a Deployment object for Busybox func (r *BusyboxReconciler) deploymentForBusybox(busybox *examplecomv1alpha1.Busybox) *appsv1.Deployment { replicas := busybox.Spec.Size dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: busybox.Name, Namespace: busybox.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: &replicas, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app": busybox.Name}, }, Template: metav1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app": busybox.Name}, }, Spec: corev1.PodSpec{ Containers: []corev1.Container{ { Name: "busybox", Image: "busybox:latest", }, }, }, }, }, } // Set the ownerRef for the Deployment, ensuring that the Deployment // will be deleted when the Busybox CR is deleted. controllerutil.SetControllerReference(busybox, dep, r.Scheme) return dep } ``` ### Explanation By setting the `OwnerReference`, if the `Busybox` resource is deleted, Kubernetes will automatically delete the `Deployment` as well. This also allows the controller to watch for changes in the `Deployment` and ensure that the desired state (such as the number of replicas) is maintained. For example, if someone modifies the `Deployment` to change the replica count to 3, while the `Busybox` CR defines the desired state as 1 replica, the controller will reconcile this and ensure the `Deployment` is scaled back to 1 replica. **Reconcile Function Example** ```go // Reconcile handles the main reconciliation loop for Busybox and the Deployment func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logf.FromContext(ctx) // Fetch the Busybox instance busybox := &examplecomv1alpha1.Busybox{} if err := r.Get(ctx, req.NamespacedName, busybox); err != nil { if apierrors.IsNotFound(err) { log.Info("Busybox resource not found. Ignoring since it must be deleted") return ctrl.Result{}, nil } log.Error(err, "Failed to get Busybox") return ctrl.Result{}, err } // Check if the Deployment already exists, if not create a new one found := &appsv1.Deployment{} err := r.Get(ctx, types.NamespacedName{Name: busybox.Name, Namespace: busybox.Namespace}, found) if err != nil && apierrors.IsNotFound(err) { // Define a new Deployment dep := r.deploymentForBusybox(busybox) log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) if err := r.Create(ctx, dep); err != nil { log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) return ctrl.Result{}, err } // Requeue the request to ensure the Deployment is created return ctrl.Result{RequeueAfter: time.Minute}, nil } else if err != nil { log.Error(err, "Failed to get Deployment") return ctrl.Result{}, err } // Ensure the Deployment size matches the desired state size := busybox.Spec.Size if *found.Spec.Replicas != size { found.Spec.Replicas = &size if err := r.Update(ctx, found); err != nil { log.Error(err, "Failed to update Deployment size", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) return ctrl.Result{}, err } // Requeue the request to ensure the correct state is achieved return ctrl.Result{Requeue: true}, nil } // Update Busybox status to reflect that the Deployment is available busybox.Status.AvailableReplicas = found.Status.AvailableReplicas if err := r.Status().Update(ctx, busybox); err != nil { log.Error(err, "Failed to update Busybox status") return ctrl.Result{}, err } return ctrl.Result{}, nil } ``` ## Watching Secondary Resources To ensure that changes to the secondary resource (such as the `Deployment`) trigger a reconciliation of the primary resource (`Busybox`), we configure the controller to watch both resources. The `Owns()` method allows you to specify secondary resources that the controller should monitor. This way, the controller will automatically reconcile the primary resource whenever the secondary resource changes (e.g., is updated or deleted). ### Example: Configuring `SetupWithManager` to Watch Secondary Resources ```go // SetupWithManager sets up the controller with the Manager. // The controller will watch both the Busybox primary resource and the Deployment secondary resource. func (r *BusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&examplecomv1alpha1.Busybox{}). // Watch the primary resource Owns(&appsv1.Deployment{}). // Watch the secondary resource (Deployment) Complete(r) } ``` ## Ensuring the Right Permissions Kubebuilder uses [markers][markers] to define RBAC permissions required by the controller. In order for the controller to properly watch and manage both the primary (`Busybox`) and secondary (`Deployment`) resources, it must have the appropriate permissions granted; i.e. to `watch`, `get`, `list`, `create`, `update`, and `delete` permissions for those resources. ### Example: RBAC Markers Before the `Reconcile` method, we need to define the appropriate RBAC markers. These markers will be used by [controller-gen][controller-gen] to generate the necessary roles and permissions when you run `make manifests`. ```go // +kubebuilder:rbac:groups=example.com,resources=busyboxes,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete ``` - The first marker gives the controller permission to manage the `Busybox` custom resource (the primary resource). - The second marker grants the controller permission to manage `Deployment` resources (the secondary resource). Note that we are granting permissions to `watch` the resources. [owner-ref-k8s-docs]: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ [cr-owner-ref-doc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil#SetOwnerReference [controller-gen]: ./../controller-gen.md [markers]:./../markers/rbac.md ================================================ FILE: docs/book/src/reference/watching-resources/secondary-resources-not-owned.md ================================================ # Watching Secondary Resources that are NOT `Owned` In some scenarios, a controller may need to watch and respond to changes in resources that it does not `Own`, meaning those resources are created and managed by another controller. The following examples demonstrate how a controller can monitor and reconcile resources that it doesn’t directly manage. This applies to any resource not `Owned` by the controller, including **Core Types** or **Custom Resources** managed by other controllers or projects and reconciled in separate processes. For instance, consider two custom resources—`Busybox` and `BackupBusybox`. If changes to `Busybox` should trigger reconciliation in the `BackupBusybox` controller, we can configure the `BackupBusybox` controller to watch for updates in `Busybox`. ### Example: Watching a Non-Owned Busybox Resource to Reconcile BackupBusybox Consider a controller that manages a custom resource `BackupBusybox` but also needs to monitor changes to `Busybox` resources across the cluster. We only want to trigger reconciliation when `Busybox` instances have the Backup feature enabled. - **Why Watch Secondary Resources?** - The `BackupBusybox` controller is not responsible for creating or owning `Busybox` resources, but changes in these resources (such as updates or deletions) directly affect the primary resource (`BackupBusybox`). - By watching `Busybox` instances with a specific label, the controller ensures that the necessary actions (e.g., backups) are triggered only for the relevant resources. ### Configuration Example Here’s how to configure the `BackupBusyboxReconciler` to watch changes in the `Busybox` resource and trigger reconciliation for `BackupBusybox`: ```go // SetupWithManager sets up the controller with the Manager. // The controller will watch both the BackupBusybox primary resource and the Busybox resource. func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&examplecomv1alpha1.BackupBusybox{}). // Watch the primary resource (BackupBusybox) Watches( &examplecomv1alpha1.Busybox{}, // Watch the Busybox CR handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { // Trigger reconciliation for the BackupBusybox in the same namespace return []reconcile.Request{ { NamespacedName: types.NamespacedName{ Name: "backupbusybox", // Reconcile the associated BackupBusybox resource Namespace: obj.GetNamespace(), // Use the namespace of the changed Busybox }, }, } }), ). // Trigger reconciliation when the Busybox resource changes Complete(r) } ``` Here’s how we can configure the controller to filter and watch for changes to only those `Busybox` resources that have the specific label: ```go // SetupWithManager sets up the controller with the Manager. // The controller will watch both the BackupBusybox primary resource and the Busybox resource, filtering by a label. func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&examplecomv1alpha1.BackupBusybox{}). // Watch the primary resource (BackupBusybox) Watches( &examplecomv1alpha1.Busybox{}, // Watch the Busybox CR handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { // Check if the Busybox resource has the label 'backup-needed: "true"' if val, ok := obj.GetLabels()["backup-enable"]; ok && val == "true" { // If the label is present and set to "true", trigger reconciliation for BackupBusybox return []reconcile.Request{ { NamespacedName: types.NamespacedName{ Name: "backupbusybox", // Reconcile the associated BackupBusybox resource Namespace: obj.GetNamespace(), // Use the namespace of the changed Busybox }, }, } } // If the label is not present or doesn't match, don't trigger reconciliation return []reconcile.Request{} }), ). // Trigger reconciliation when the labeled Busybox resource changes Complete(r) } ``` ================================================ FILE: docs/book/src/reference/watching-resources.md ================================================ # Watching Resources When extending the Kubernetes API, we aim to ensure that our solutions behave consistently with Kubernetes itself. For example, consider a `Deployment` resource, which is managed by a controller. This controller is responsible for responding to changes in the cluster—such as when a `Deployment` is created, updated, or deleted—by triggering reconciliation to ensure the resource’s state matches the desired state. Similarly, when developing our controllers, we want to watch for relevant changes in resources that are crucial to our solution. These changes—whether creations, updates, or deletions—should trigger the reconciliation loop to take appropriate actions and maintain consistency across the cluster. The [controller-runtime][controller-runtime] library provides several ways to watch and manage resources. ## Primary Resources The **Primary Resource** is the resource that your controller is responsible for managing. For example, if you create a custom resource definition (CRD) for `MyApp`, the corresponding controller is responsible for managing instances of `MyApp`. In this case, `MyApp` is the **Primary Resource** for that controller, and your controller’s reconciliation loop focuses on ensuring the desired state of these primary resources is maintained. When you create a new API using Kubebuilder, the following default code is scaffolded, ensuring that the controller watches all relevant events—such as creations, updates, and deletions—for (`For()`) the new API. This setup guarantees that the reconciliation loop is triggered whenever an instance of the API is created, updated, or deleted: ```go // Watches the primary resource (e.g., MyApp) for create, update, delete events if err := ctrl.NewControllerManagedBy(mgr). For(&{}). <-- See there that the Controller is For this API Complete(r); err != nil { return err } ``` ## Secondary Resources Your controller will likely also need to manage **Secondary Resources**, which are the resources required on the cluster to support the **Primary Resource**. Changes to these **Secondary Resources** can directly impact the **Primary Resource**, so the controller must watch and reconcile these resources accordingly. ### Which are Owned by the Controller These **Secondary Resources**, such as `Services`, `ConfigMaps`, or `Deployments`, when `Owned` by the controllers, are created and managed by the specific controller and are tied to the **Primary Resource** via [OwnerReferences][owner-ref-k8s-docs]. For example, if we have a controller to manage our CR(s) of the Kind `MyApp` on the cluster, which represents our application solution, all resources required to ensure that `MyApp` is up and running with the desired number of instances will be **Secondary Resources**. The code responsible for creating, deleting, and updating these resources will be part of the `MyApp` Controller. We would add the appropriate [OwnerReferences][owner-ref-k8s-docs] using the [controllerutil.SetControllerReference][cr-owner-ref-doc] function to indicate that these resources are owned by the same controller responsible for managing `MyApp` instances, which will be reconciled by the `MyAppReconciler`. Additionally, if the **Primary Resource** is deleted, Kubernetes' garbage collection mechanism ensures that all associated **Secondary Resources** are automatically deleted in a cascading manner. ### Which are NOT `Owned` by the Controller Note that **Secondary Resources** can either be APIs/CRDs defined in your project or in other projects that are relevant to the **Primary Resources**, but which the specific controller is not responsible for creating or managing. For example, if we have a CRD that represents a backup solution (i.e. `MyBackup`) for our `MyApp`, it might need to watch changes in the `MyApp` resource to trigger reconciliation in `MyBackup` to ensure the desired state. Similarly, `MyApp`'s behavior might also be impacted by CRDs/APIs defined in other projects. In both scenarios, these resources are treated as **Secondary Resources**, even if they are not `Owned` (i.e., not created or managed) by the `MyAppController`. In Kubebuilder, resources that are not defined in the project itself and are not a **Core Type** (those not defined in the Kubernetes API) are called **External Types**. An **External Type** refers to a resource that is not defined in your project but one that you need to watch and respond to. For example, if **Operator A** manages a `MyApp` CRD for application deployment, and **Operator B** handles backups, **Operator B** can watch the `MyApp` CRD as an external type to trigger backup operations based on changes in `MyApp`. In this scenario, **Operator B** could define a `BackupConfig` CRD that relies on the state of `MyApp`. By treating `MyApp` as a **Secondary Resource**, **Operator B** can watch and reconcile changes in **Operator A**'s `MyApp`, ensuring that backup processes are initiated whenever `MyApp` is updated or scaled. ## General Concept of Watching Resources Whether a resource is defined within your project or comes from an external project, the concept of **Primary** and **Secondary Resources** remains the same: - The **Primary Resource** is the resource the controller is primarily responsible for managing. - **Secondary Resources** are those that are required to ensure the primary resource works as desired. Therefore, regardless of whether the resource was defined by your project or by another project, your controller can watch, reconcile, and manage changes to these resources as needed. ## Why does watching the secondary resources matter? When building a Kubernetes controller, it’s crucial to not only focus on **Primary Resources** but also to monitor **Secondary Resources**. Failing to track these resources can lead to inconsistencies in your controller's behavior and the overall cluster state. Secondary resources may not be directly managed by your controller, but changes to these resources can still significantly impact the primary resource and your controller's functionality. Here are the key reasons why it's important to watch them: - **Ensuring Consistency**: - Secondary resources (e.g., child objects or external dependencies) may diverge from their desired state. For instance, a secondary resource may be modified or deleted, causing the system to fall out of sync. - Watching secondary resources ensures that any changes are detected immediately, allowing the controller to reconcile and restore the desired state. - **Avoiding Random Self-Healing**: - Without watching secondary resources, the controller may "heal" itself only upon restart or when specific events are triggered. This can cause unpredictable or delayed reactions to issues. - Monitoring secondary resources ensures that inconsistencies are addressed promptly, rather than waiting for a controller restart or external event to trigger reconciliation. - **Effective Lifecycle Management**: - Secondary resources might not be owned by the controller directly, but their state still impacts the behavior of primary resources. Without watching these, you risk leaving orphaned or outdated resources. - Watching non-owned secondary resources lets the controller respond to lifecycle events (create, update, delete) that might affect the primary resource, ensuring consistent behavior across the system. See [Watching Secondary Resources That Are Not Owned](./watching-resources/secondary-resources-not-owned.md#configuration-example) for an example. ## Why not use `RequeueAfter X` for all scenarios instead of watching resources? Kubernetes controllers are fundamentally **event-driven**. When creating a controller, the **Reconciliation Loop** is typically triggered by **events** such as `create`, `update`, or `delete` actions on resources. This event-driven approach is more efficient and responsive compared to constantly requeuing or polling resources using `RequeueAfter`. This ensures that the system only takes action when necessary, maintaining both performance and efficiency. In many cases, **watching resources** is the preferred approach for ensuring Kubernetes resources remain in the desired state. It is more efficient, responsive, and aligns with Kubernetes' event-driven architecture. However, there are scenarios where `RequeueAfter` is appropriate and necessary, particularly for managing external systems that do not emit events or for handling resources that take time to converge, such as long-running processes. Relying solely on `RequeueAfter` for all scenarios can lead to unnecessary overhead and delayed reactions. Therefore, it is essential to prioritize **event-driven reconciliation** by configuring your controller to **watch resources** whenever possible, and reserving `RequeueAfter` for situations where periodic checks are required. ### When `RequeueAfter X` is Useful While `RequeueAfter` is not the primary method for triggering reconciliations, there are specific cases where it is necessary, such as: - **Observing External Systems**: When working with external resources that do not generate events (e.g., external databases or third-party services), `RequeueAfter` allows the controller to periodically check the status of these resources. - **Time-Based Operations**: Some tasks, such as rotating secrets or renewing certificates, must happen at specific intervals. `RequeueAfter` ensures these operations are performed on schedule, even when no other changes occur. - **Handling Errors or Delays**: When managing resources that encounter errors or require time to self-heal, `RequeueAfter` ensures the controller waits for a specified duration before checking the resource’s status again, avoiding constant reconciliation attempts. ## Usage of Predicates For more complex use cases, [Predicates][cr-predicates] can be used to fine-tune when your controller should trigger reconciliation. Predicates allow you to filter events based on specific conditions, such as changes to particular fields, labels, or annotations, ensuring that your controller only responds to relevant events and operates efficiently. [controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime [owner-ref-k8s-docs]: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ [cr-predicates]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/predicate [secondary-resources-doc]: watching-resources/secondary-owned-resources [predicates-with-external-type-doc]: watching-resources/predicates-with-watch [cr-owner-ref-doc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil#SetOwnerReference ================================================ FILE: docs/book/src/reference/webhook-bootstrap-problem.md ================================================ # Webhook Bootstrap Problem ## The Problem When you create a webhook for a **core Kubernetes type** (Pod, Deployment, Job, etc.), the webhook can block its own controller Pod from starting, causing a deployment deadlock. **Example command:** ```bash kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation ``` **Example scenario:** 1. You create a validating webhook for Pods 2. You deploy your controller (which runs in a Pod) 3. Kubernetes tries to create your controller Pod 4. Your webhook intercepts this Pod creation 5. The webhook server isn't ready yet (it's inside the Pod being created) 6. The Pod creation hangs waiting for webhook validation 7. The webhook never starts because the Pod is blocked **Result:** Deadlock. Your deployment fails. ## When Does This Occur? ### Core Kubernetes Types The bootstrap problem occurs when creating webhooks for built-in Kubernetes resources: - `core` group: Pod, Service, Namespace, ConfigMap, Secret - `apps` group: Deployment, StatefulSet, DaemonSet, ReplicaSet - `batch` group: Job, CronJob - Other built-in types **Why?** Your webhook validates the same type of resource that your controller deployment creates (typically Pods or Deployments). ### Custom CRDs The bootstrap problem **does not occur** with custom resource webhooks: - Your webhook validates `MyResource` objects - Your controller runs as a Pod - Pods and MyResources are different types - No circular dependency ## How to Fix Configure your webhook to **skip validating its own resources** using either `namespaceSelector` or `objectSelector`. ### Option 1: namespaceSelector (Recommended) Exclude the entire namespace where your webhook runs. **Step 1:** Add label to the Namespace in `config/manager/manager.yaml`: ```yaml apiVersion: v1 kind: Namespace metadata: labels: control-plane: controller-manager app.kubernetes.io/name: my-project app.kubernetes.io/managed-by: kustomize webhook-excluded: "true" name: system ``` **Step 2:** Create patch file `config/webhook/namespaceselector_patch.yaml`: ```yaml # For mutating webhooks (--defaulting) - op: add path: /webhooks/0/namespaceSelector value: matchExpressions: - key: webhook-excluded operator: DoesNotExist ``` For validating webhooks (`--programmatic-validation`), create a similar patch targeting `ValidatingWebhookConfiguration`. **Step 3:** Add patch to `config/webhook/kustomization.yaml`: ```yaml resources: - manifests.yaml - service.yaml patches: - path: namespaceselector_patch.yaml target: group: admissionregistration.k8s.io version: v1 kind: MutatingWebhookConfiguration name: mutating-webhook-configuration ``` **Step 4:** Deploy: ```bash make deploy IMG= ``` ### Option 2: objectSelector Exclude specific labeled Pods from webhook validation. **Step 1:** Add label to Pods in `config/manager/manager.yaml`: ```yaml spec: template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: control-plane: controller-manager app.kubernetes.io/name: my-project webhook-excluded: "true" ``` **Step 2-4:** Same as Option 1, but use `objectSelector` instead of `namespaceSelector` in the patch file. ### Multiple Webhooks If you created webhooks for multiple core types (e.g., Pod and Deployment), you'll have multiple webhook entries. **Check webhook count:** ```bash make manifests grep " name: m" config/webhook/manifests.yaml # Count mutating webhooks grep " name: v" config/webhook/manifests.yaml # Count validating webhooks ``` **Example output:** ``` name: mpod-v1.kb.io # Index 0 name: mdeployment-v1.kb.io # Index 1 ``` **Add patches for all indices** in your patch file: ```yaml - op: add path: /webhooks/0/namespaceSelector value: matchExpressions: - key: webhook-excluded operator: DoesNotExist - op: add path: /webhooks/1/namespaceSelector value: matchExpressions: - key: webhook-excluded operator: DoesNotExist ``` ### Mixed Webhooks (CRD + Core Types) If you have both custom CRD webhooks and core type webhooks: - CRD webhooks appear first in the configuration - Core type webhooks appear after - Count **all** webhooks and add patches for the indices of your core type webhooks **Example:** If you have 1 CRD webhook (index 0) and 1 core type webhook (index 1), your patch should target index 1: ```yaml - op: add path: /webhooks/1/namespaceSelector value: matchExpressions: - key: webhook-excluded operator: DoesNotExist ``` ## Choosing Between namespaceSelector and objectSelector | Feature | namespaceSelector | objectSelector | |---------|-------------------|----------------| | Excludes | Entire namespace | Specific pods | | Scope | Broad | Fine-grained | | Best for | Dedicated webhook namespace | Shared namespace | | Complexity | Simple | More targeted | **Recommendation:** Use `namespaceSelector` unless you need fine-grained control. ## References - [Kubernetes Admission Webhook Best Practices](https://kubernetes.io/docs/concepts/cluster-administration/admission-webhooks-good-practices/) - [namespaceSelector API Reference](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-namespaceselector) - [objectSelector API Reference](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#matching-requests-objectselector) ================================================ FILE: docs/book/src/reference/webhook-overview.md ================================================ # Webhook Webhooks are requests for information sent in a blocking fashion. A web application implementing webhooks will send a HTTP request to other applications when a certain event happens. In the kubernetes world, there are 3 kinds of webhooks: [admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#admission-webhooks), [authorization webhook](https://kubernetes.io/docs/reference/access-authn-authz/webhook/) and [CRD conversion webhook](https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/#webhook-conversion). In [controller-runtime](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook?tab=doc) libraries, we support admission webhooks and CRD conversion webhooks. Kubernetes supports these dynamic admission webhooks as of version 1.9 (when the feature entered beta). Kubernetes supports the conversion webhooks as of version 1.15 (when the feature entered beta). ================================================ FILE: docs/book/src/versions_compatibility_supportability.md ================================================ # Versions Compatibility and Supportability Projects created by Kubebuilder contain a `Makefile` that installs tools at versions defined during project creation. The main tools included are: - [kustomize](https://github.com/kubernetes-sigs/kustomize) - [controller-gen](https://github.com/kubernetes-sigs/controller-tools) - [setup-envtest](https://github.com/kubernetes-sigs/controller-runtime/tree/main/tools/setup-envtest) Additionally, these projects include a `go.mod` file specifying dependency versions. Kubebuilder relies on [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) and its Go and Kubernetes dependencies. Therefore, the versions defined in the `Makefile` and `go.mod` files are the ones that have been tested, supported, and recommended. Each minor version of Kubebuilder is tested with a specific minor version of client-go. While a Kubebuilder minor version *may* be compatible with other client-go minor versions, or other tools this compatibility is not guaranteed, supported, or tested. The minimum Go version required by Kubebuilder is determined by the highest minimum Go version required by its dependencies. This is usually aligned with the minimum Go version required by the corresponding `k8s.io/*` dependencies. Compatible `k8s.io/*` versions, client-go versions, and minimum Go versions can be found in the `go.mod` file scaffolded for each project for each [tag release](https://github.com/kubernetes-sigs/kubebuilder/tags). **Example:** For the `4.1.1` release, the minimum Go version compatibility is `1.22`. You can refer to the samples in the testdata directory of the tag released [v4.1.1](https://github.com/kubernetes-sigs/kubebuilder/tree/v4.1.1/testdata), such as the [go.mod](https://github.com/kubernetes-sigs/kubebuilder/blob/v4.1.1/testdata/project-v4/go.mod#L3) file for `project-v4`. You can also check the tools versions supported and tested for this release by examining the [Makefile](https://github.com/kubernetes-sigs/kubebuilder/blob/v4.1.1/testdata/project-v4/Makefile#L160-L165). ## Operating Systems Supported Currently, Kubebuilder officially supports macOS and Linux platforms. If you are using a Windows OS, we recommend you read the instructions in [here](https://github.com/kubernetes-sigs/kubebuilder/blob/master/docs/windows.md). Contributions towards supporting Windows are not planned. [basic-project-doc]: ./cronjob-tutorial/basic-project.md ================================================ FILE: docs/book/theme/css/custom.css ================================================ /* Adds a thin border to the sidebar (aesthetics) */ #mdbook-sidebar { border-right: 1px solid var(--theme-popup-border); } /* Sets the size of the logo on the menu bar */ .large-logo-img { height: 50px; } /* Centers the logo on the menu bar */ .menu-title { display: flex; align-items: center; /* vertical centering */ justify-content: center; /* horizontal centering */ } /* Fixes first header sliding under the menu bar when selected */ .content { overflow-y: clip; } /* Fixes contrast in codeblocks */ pre > .hljs { border: 1px solid var(--theme-popup-border); border-radius: 6px; } /* Fixes scrollbar background color */ html { scrollbar-color: var(--scrollbar) transparent; } /* Fixes links formatting */ .content a { text-decoration: solid underline var(--links); } /* Hides excessive theme options */ #mdbook-theme-default_theme, #mdbook-theme-rust, #mdbook-theme-coal, #mdbook-theme-ayu { display: none; } /* custom light theme */ .light { --sidebar-bg: #eef5ff; --sidebar-fg: #334155; --sidebar-non-existant: #7b8da8; --sidebar-spacer: #c9d3e0; } ================================================ FILE: docs/book/theme/css/markers.css ================================================ /* From here on out is custom stuff */ /* marker docs styles */ /* NB(directxman12): The general gist of this is that we use semantic markup * for the actual HTML as much as possible, and then use CSS to look pretty and * extract the actual relevant information. Theoretically, this'll let us do * stuff like transform the information for different screen widths. */ /* Removes the counter from marker definitions */ dd:has(+ dd)::before, dd + dd::before { content: normal; } /* the marker */ .marker { display: flex; flex-wrap: wrap; align-items: center; margin-bottom: 0.25em; } .marker > dt.name { font-weight: bold; } /* the target blob */ .marker::before { content: "on " attr(data-target); padding: 1px 6px; border-radius: 20%; background: var(--quote-bg); margin-left: 0.5em; font-weight: normal; opacity: 0.75; font-size: 0.75em; order: 2; /* hack around the ::before's positioning to get it after the line */ } /* deprecated markers */ .marker.deprecated[data-target] { /* use attribute marker for specificity */ order: 4; opacity: 0.65; } .marker.deprecated::before { content: "deprecated (on " attr(data-target) ")"; color: red; } .marker.deprecated:not([data-deprecated=""])::before { content: "use " attr(data-deprecated) " (on " attr(data-target) ")"; color: red; } /* the summary arguments (hidden in non-summary view) */ .marker dd.args { margin-left: 0; font-family: mono; order: 1; /* hack around the ::before's positioning to get it after the line */ } .marker dl.args.summary { display: inline-block; margin-bottom: 0; margin-top: 0; } /* TODO(directxman12): optional */ .marker dl.args.summary dt { display: inline-block; font-style: inherit; } .marker dl.args.summary dt:first-child::before { content: ":"; } .marker dl.args.summary dt::before { content: ","; } /* hide in non-summary view */ .marker dd.args { display: none; } /* the description */ .marker dd.description { order: 3; /* hack around the ::before's positioning to get it after the line */ width: 100%; display: flex; flex-direction: column; } /* all arguments */ .marker dl.args dt.argument::after { content: "="; } .marker dl.args dd.type { font-style: italic; } .marker .argument { display: inline-block; margin-left: 0; } .marker .argument.type { font-size: 0.875em; } .marker .literal { font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace; font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */ } .marker .argument.type::before { content: "‹"; } .marker .argument.type::after { content: "›"; } /* summary args */ .marker .args.summary .argument.optional { opacity: 0.75; } /* anonymous marker args */ .marker.anonymous .description details { order: 1; flex: 1; /* don't cause arg syntax to wrap */ } .marker.anonymous .description .args { order: 0; /* go before the description */ /* all on a single line */ margin-top: 0; margin-bottom: 0; margin-right: 1em; } .marker.anonymous .description { flex-direction: row; } .marker .description dl.args:empty { margin-top: 0; } .marker .type .slice::before { content: "[]"; } /* description args */ .marker .description dt.argument.optional::before { content: "opt"; padding: 1px 4px; border-radius: 20%; background: var(--quote-bg); opacity: 0.5; margin-left: -3em; float: left; } /* help text */ .marker summary.no-details { list-style: none; } .marker summary.no-details::-webkit-details-marker { display: none; } /* summary view */ .markers-summarize:checked ~ dl > .marker dd.args { display: inline-block; } .markers-summarize:checked ~ dl > .marker dd.description dl.args { display: none; } .markers-summarize:checked ~ dl > .marker dd.description { margin-bottom: 0.25em; } input.markers-summarize { display: none; } label.markers-summarize::before { margin-right: 0.5em; content: "\25bc"; display: inline-block; } input.markers-summarize:checked ~ label.markers-summarize::before { content: "\25b6"; } /* misc */ /* marker details should be indented to be in line with the summary, * which is indented due to the expando */ .marker details > p { margin-left: 1em; } /* sort by target */ .marker[data-target="package"] { order: 2; } .marker[data-target="type"] { order: 1; } .marker[data-target="field"] { order: 0; } .markers { display: flex; flex-direction: column; } /* don't add margin between collapsed code elements and pre blocks */ /* (this just removes all margin around pre, but that's generally fine) */ .literate pre { margin: 0; } .literate cite.literate-source + * { /* a bit of margin against the cite element */ margin-top: 0.125em; } /* Completely hide low-value collapsed sections (license text, imports) */ details.collapse-hide { display: none; } /* details elements (not markers) */ details.collapse-code > summary { width: 100%; cursor: pointer; display: flex; box-sizing: border-box; /* why isn't this the default? :-/ */ } details.collapse-code > summary::after { content: "\25c0"; float: right; font-size: 0.875em; color: var(--inline-code-color); opacity: 0.8; } details.collapse-code[open] > summary::after { content: "\25bc"; } details.collapse-code > summary pre { flex: 1; box-sizing: border-box; /* why isn't this the default? :-/ */ margin: inherit; padding: 0.25em 0.5em; } details.collapse-code > summary pre span::after { content: " (hidden)"; font-size: 80%; } details.collapse-code[open] > summary pre span::after { content: ""; } details.collapse-code > summary pre span.collapse-summary::before { content: "// "; } details.collapse-code > summary pre span::before { content: "// "; } /* make summary into code a bit nicer looking */ details.collapse-code[open] > summary + pre { margin-top: 0; } /* get rid of the ugly blue box that makes the summary->code look bad */ details.collapse-code summary:focus { outline: none; font-weight: bold; /* keep something around for tab users */ } /* don't show the default expando */ details.collapse-code > summary { list-style: none; } details.collapse-code > summary::-webkit-details-marker { display: none; } /* diagrams */ .diagrams { display: flex; flex-direction: row; align-items: center; } .diagrams > * { margin-left: 1em; margin-right: 1em; font-size: 160%; font-weight: bold; } .diagrams object, .diagrams svg { max-width: 100%; max-height: 10em; /* force svg height to behave */ } .diagrams path, .diagrams polyline, .diagrams circle { stroke: var(--fg); } .diagrams path.text { fill: var(--fg); stroke: none; } .diagrams path.text.invert { fill: black; stroke: none; } /* notes */ aside.note { border: 1px solid var(--searchbar-border-color); border-radius: 3px; margin-top: 1em; } aside.note > * { margin-left: 1em; margin-right: 1em; } /* note title */ aside.note > h1 { border-bottom: 1px solid var(--searchbar-border-color); margin: 0; padding: 0.5em 1em; font-size: 100%; font-weight: normal; background: var(--quote-bg); } /* warning notes */ aside.note.warning > h1 { background: var(--warning-note-background-color, #fcf8f2); } aside.note.warning > h1::before { /* TODO(directxman12): fill in these colors in theme. * If you're good with colors, feel free to play around with this * in dark mode. */ content: "!"; color: var(--warning-note-color, #f0ad4e); margin-right: 1em; font-size: 100%; vertical-align: middle; font-weight: bold; padding-left: 0.6em; padding-right: 0.6em; border-radius: 50%; border: 2px solid var(--warning-note-color, #f0ad4e); } /* literate source citations */ cite.literate-source { font-size: 75%; font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace; } cite.literate-source::before { content: "$ "; font-weight: bold; font-style: normal; } cite.literate-source > a::before { content: "vim "; font-style: normal; color: var(--fg); } /* add a bit of extra padding for readability */ .literate pre code { padding-top: 0.75em; padding-bottom: 0.75em; } ================================================ FILE: docs/book/theme/css/version-dropdown.css ================================================ .version-dropdown-content { display: none; position: absolute; background-color: #f9f9f9; min-width: 90px; box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); z-index: 1; } .version-dropdown-content a { color: black; padding: 12px 16px; text-decoration: none; display: block; } .version-dropdown-content a:hover { background-color: #f1f1f1; } .version-dropdown:hover .version-dropdown-content { display: block; } ================================================ FILE: docs/book/theme/index.hbs ================================================ {{ title }} {{#if is_print }} {{/if}} {{#if base_url}} {{/if}} {{> head}} {{#if favicon_svg}} {{/if}} {{#if favicon_png}} {{/if}} {{#if print_enable}} {{/if}} {{#each additional_css}} {{/each}} {{#if mathjax_support}} {{/if}}

Keyboard shortcuts

Press or to navigate between chapters

{{#if search_enabled}}

Press S or / to search in the book

{{/if}}

Press ? to show this help

Press Esc to hide this help

{{> header}}
{{#if search_enabled}} {{/if}}
{{#if live_reload_endpoint}} {{/if}} {{#if playground_line_numbers}} {{/if}} {{#if playground_copyable}} {{/if}} {{#if playground_js}} {{/if}} {{#if search_js}} {{/if}} {{#each additional_js}} {{/each}} {{#if is_print}} {{#if mathjax_support}} {{else}} {{/if}} {{/if}} {{#if fragment_map}} {{/if}}
================================================ FILE: docs/book/utils/go.mod ================================================ module sigs.k8s.io/kubebuilder/docs/book/utils go 1.24.6 ================================================ FILE: docs/book/utils/go.sum ================================================ ================================================ FILE: docs/book/utils/litgo/literate.go ================================================ /* Copyright 2019 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 main import ( "fmt" "go/scanner" "go/token" "log" "net/url" "os" "path" "path/filepath" "strings" "unicode" "sigs.k8s.io/kubebuilder/docs/book/utils/plugin" ) // Literate is a plugin that extracts block comments from Go source and // interleaves them with the surrounding code as fenced code blocks. // It should support all output formats. // It's triggered by using the an expression like `{{#literatego ./path/to/source/file.go}}`. // The marker `+kubebuilder:docs-gen:collapse=` can be used to collapse a description/code // pair into a details block with the given summary. type Literate struct { // PrettyPathPrunePrefix specifies the prefix, if any to prune off of user-visible paths PrettyPathPrunePrefix string // BaseSourcePath specifies the base path to internet-reachable versions of the source code used BaseSourcePath *url.URL } // SupportsOutput implements plugin.Plugin func (Literate) SupportsOutput(_ string) bool { return true } // Process implements plugin.Plugin func (l Literate) Process(input *plugin.Input) error { srcDir := input.Context.Config.Book.Src if srcDir == "" { srcDir = "src" // mdBook 0.5.0 no longer populates this field } bookSrcDir := filepath.Join(input.Context.Root, srcDir) return plugin.EachCommand(&input.Book, "literatego", func(chapter *plugin.BookChapter, relPath string) (string, error) { chapterDir := filepath.Dir(chapter.Path) pathInfo := filePathInfo{ chapterRelativePath: relPath, chapterDir: chapterDir, bookSrcDir: bookSrcDir, } fullPath := pathInfo.FullPath() // TODO(directxman12): don't escape root? contents, err := os.ReadFile(fullPath) if err != nil { return "", fmt.Errorf("unable to import %q: %v", fullPath, err) } return l.extractContents(contents, pathInfo) }) } // filePathInfo stores different paths to a file, to allow for nicely // displaying relative path information. type filePathInfo struct { // chapterRelativePath is the path relative to the current chapter file chapterRelativePath string // chapterDir is the directory of the chapter, relative to bookSrcDir chapterDir string // bookSrcDir is the absolute book source path bookSrcDir string } // FullPath returns the full, absolute path to the given file on the source filesystem. func (f filePathInfo) FullPath() string { return filepath.Join(f.bookSrcDir, f.chapterDir, f.chapterRelativePath) } // viewablePath returns the internet-viewable path to the given source file func (f filePathInfo) ViewablePath(baseBookSrcURL url.URL) string { relPath := filepath.ToSlash(filepath.Join(f.chapterDir, f.chapterRelativePath)) outURL := baseBookSrcURL outURL.Path = path.Join(outURL.Path, relPath) return outURL.String() } // commentCodePair represents a block of code with some text before it, optionally // marked as collapsed with the given "collapse summary". type commentCodePair struct { comment string code string collapse string } // collapsePrefix is the marker comment that indicates that the previous commentCodePair // should be collapsed with the given summary var collapsePrefix = "+kubebuilder:docs-gen:collapse=" // getCollapse checks if the given token is a collapse marker, and // extracts the summary if so. func getCollapse(tok token.Token, lit string) string { if tok != token.COMMENT { return "" } if lit[:2] != "//" { return "" } rest := strings.TrimSpace(lit[2:]) if !strings.HasPrefix(rest, collapsePrefix) { return "" } return rest[len(collapsePrefix):] } // isBlockComment checks that the given token is a `/* comment */`-style comment, // which we consider to be the start of a codeCommentPair func isBlockComment(tok token.Token, lit string) bool { if tok != token.COMMENT { return false } if len(lit) < 3 || lit[0] != '/' || lit[1] != '*' { return false } return true } // commentText extracts the text from the given comment, slicing off // some common amount of whitespace prefix. func commentText(raw string, lineOffset int) string { rawBody := raw[2 : len(raw)-2] // chop of the delimiters lines := strings.Split(rawBody, "\n") if len(lines) == 0 { return "" } for i, line := range lines { offset := lineOffset if len(line) < offset { offset = len(line) } lines[i] = strings.TrimLeftFunc(line[:offset], unicode.IsSpace) + line[offset:] } return strings.Join(lines, "\n") } // extractPairs extracts all commentCodePairs from the given source code with // the given path. A block starts as soon as the last block ends (or at the // beginning of the file), and ends as soon as a block comment is encountered, // or if a collapse marker is encountered. func extractPairs(contents []byte, path string) ([]commentCodePair, error) { fileSet := token.NewFileSet() file := fileSet.AddFile(path, -1, len(contents)) scan := scanner.Scanner{} var errs []error scan.Init(file, contents, func(pos token.Position, msg string) { errs = append(errs, fmt.Errorf("error parsing file %s: %s", pos, msg)) }, scanner.ScanComments) // grab all the different sections var pairs []commentCodePair var lastPair commentCodePair lastCodeBlockStart := 0 var tok token.Token for tok != token.EOF { var pos token.Pos var lit string pos, tok, lit = scan.Scan() collapse := getCollapse(tok, lit) if collapse != "" { lastPair.collapse = collapse } if collapse == "" && !isBlockComment(tok, lit) { continue } codeEnd := file.Offset(pos) - 1 if codeEnd-lastCodeBlockStart > 0 { lastPair.code = string(contents[lastCodeBlockStart:codeEnd]) } pairs = append(pairs, lastPair) if collapse == "" { line := file.Line(pos) lineStart := file.LineStart(line) lastPair = commentCodePair{ comment: commentText(lit, file.Offset(pos)-file.Offset(lineStart)), } } else { lastPair = commentCodePair{} } lastCodeBlockStart = file.Offset(pos) + len(lit) } lastPair.code = string(contents[lastCodeBlockStart:]) pairs = append(pairs, lastPair) if len(errs) > 0 { return nil, errs[0] } return pairs, nil } // extractContents extracts comment-code pairs from the given named file // contents, and then renders the result to markdown. func (l Literate) extractContents(contents []byte, pathInfo filePathInfo) (string, error) { pairs, err := extractPairs(contents, pathInfo.FullPath()) if err != nil { return "", err } out := new(strings.Builder) out.WriteString(`
`) // write the source so that readers can easily find the code sourcePath := pathInfo.ViewablePath(*l.BaseSourcePath) prettyPath := pathInfo.chapterRelativePath if l.PrettyPathPrunePrefix != "" { prunedPath, err := filepath.Rel(l.PrettyPathPrunePrefix, prettyPath) if err != nil { return "", fmt.Errorf("unable to remove path prefix %q from %q: %v", l.PrettyPathPrunePrefix, prettyPath, err) } prettyPath = prunedPath } out.WriteString(fmt.Sprintf(`%[2]s`, sourcePath, prettyPath)) for _, pair := range pairs { if pair.collapse != "" { collapseClass := "collapse-code" // Hide low-value sections entirely (licenses, imports, etc) if strings.EqualFold(pair.collapse, "Apache License") || strings.EqualFold(pair.collapse, "Imports") { collapseClass = "collapse-code collapse-hide" } out.WriteString("
")
			out.WriteString(pair.collapse)
			out.WriteString("
") } if strings.TrimSpace(pair.comment) != "" { out.WriteString("\n") out.WriteString(removeIndent(pair.comment)) } if strings.TrimSpace(pair.code) != "" { out.WriteString("\n\n```go") out.WriteString(wrapWithNewlines(pair.code)) out.WriteString("```\n") } if pair.collapse != "" { out.WriteString("\n
") } // TODO(directxman12): nice side-by-side sections } out.WriteString(`
`) return out.String(), nil } // removeIndent removes any initial indent that gofmt puts in place, // because it likes to make our lives harder. // // If we left them in place, text would turn into legacy markdown codeblocks. func removeIndent(comment string) string { lines := strings.Split(comment, "\n") for i, line := range lines { if strings.HasPrefix(line, "\t") { lines[i] = line[1:] } } return strings.Join(lines, "\n") } // wrapWithNewlines ensures that we begin and end with a newline character. func wrapWithNewlines(src string) string { src = strings.Trim(src, "\n") // remove newlines first to avoid too many return "\n" + src + "\n" } func main() { baseURL, err := url.Parse("https://sigs.k8s.io/kubebuilder/docs/book/src") if err != nil { log.Fatal(err.Error()) } cfg := Literate{ PrettyPathPrunePrefix: "testdata", BaseSourcePath: baseURL, } if err := plugin.Run(cfg, os.Stdin, os.Stdout, os.Args[1:]...); err != nil { log.Fatal(err.Error()) } } ================================================ FILE: docs/book/utils/markerdocs/doctypes.go ================================================ /* Copyright 2019 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 main // these should be kept in sync with the output from controller-tools type DetailedHelp struct { Summary string `json:"summary"` Details string `json:"details"` } type Argument struct { Type string `json:"type"` Optional bool `json:"optional"` ItemType *Argument `json:"itemType"` } type FieldHelp struct { // definition Name string `json:"name"` Argument `json:",inline"` // help DetailedHelp `json:",inline"` } type MarkerDoc struct { // definition Name string `json:"name"` Target string `json:"target"` // help DetailedHelp `json:",inline"` Category string `json:"category"` DeprecatedInFavorOf *string `json:"deprecatedInFavorOf"` Fields []FieldHelp `json:"fields"` } type CategoryDoc struct { Category string `json:"category"` Markers []MarkerDoc `json:"markers"` } func (m MarkerDoc) Anonymous() bool { return len(m.Fields) == 1 && m.Fields[0].Name == "" } ================================================ FILE: docs/book/utils/markerdocs/html.go ================================================ /* Copyright 2019 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 main import ( "fmt" "html" "io" "strings" ) // NB(directxman12): we use this instead of templates to avoid // weird issues with whitespace in elements rendered as inline. // Writing with templates was getting tricky to do without // compromising readability. // // This isn't an amazing solution, but it's good enough™ // toHTML knows how to write itself as HTML to an output. type toHTML interface { // WriteHTML writes this as HTML to the given Writer. WriteHTML(io.Writer) error } // Text is a chunk of text in an HTML doc. type Text string // WriteHTML writes the string as HTML to the given Writer while accounting for mdBook's handling // of backticks. Given mdBook's behavior of treating backticked content as raw text, this function // ensures proper rendering by preventing unnecessary HTML escaping within code snippets. Chunks // outside of backticks are HTML-escaped for security, while chunks inside backticks remain as raw // text, preserving mdBook's intended rendering of code content. func (t Text) WriteHTML(w io.Writer) error { textChunks := strings.Split(string(t), "`") for i, chunk := range textChunks { if i%2 == 0 { // Outside backticks, escape and write HTML _, err := io.WriteString(w, html.EscapeString(chunk)) if err != nil { return err } } else { // Inside backticks, write raw HTML _, err := io.WriteString(w, "`"+chunk+"`") if err != nil { return err } } } return nil } // Tag is some tag with contents and attributes in an HTML doc. type Tag struct { Name string Attrs Attrs Children []toHTML } // WriteHTML writes the tag as HTML to the given Writer func (t Tag) WriteHTML(w io.Writer) error { attrsOut := "" if t.Attrs != nil { attrsOut = t.Attrs.ToAttrs() } if _, err := fmt.Fprintf(w, "<%s %s>", t.Name, attrsOut); err != nil { return err } for _, child := range t.Children { if err := child.WriteHTML(w); err != nil { return err } } if _, err := fmt.Fprintf(w, "", t.Name); err != nil { return err } return nil } // Fragment is some series of tags, text, etc in an HTML doc. type Fragment []toHTML // WriteHTML writes the fragment as HTML to the given Writer func (f Fragment) WriteHTML(w io.Writer) error { for _, item := range f { if err := item.WriteHTML(w); err != nil { return err } } return nil } // Attrs knows how to convert itself to HTML attributes. type Attrs interface { // ToAttrs returns `key1="value1" key2="value2"` etc to be placed into an HTML tag. ToAttrs() string } // classes sets the class attribute to these class names. type classes []string // ToAttrs implements Attrs func (c classes) ToAttrs() string { return fmt.Sprintf("class=%q", strings.Join(c, " ")) } // optionalClasses sets the class attribute to these class names, if their values are true. type optionalClasses map[string]bool // ToAttrs implements Attrs func (c optionalClasses) ToAttrs() string { actualClasses := make([]string, 0, len(c)) for class, active := range c { if active { actualClasses = append(actualClasses, class) } } return classes(actualClasses).ToAttrs() } // attrs joins together one or more Attrs. type attrs []Attrs // ToAttrs implements Attrs func (a attrs) ToAttrs() string { parts := make([]string, len(a)) for i, attr := range a { parts[i] = attr.ToAttrs() } return strings.Join(parts, " ") } // dataAttr represents some `data-*` attribute. type dataAttr struct { Name string Value string } // ToAttrs implements Attrs func (d dataAttr) ToAttrs() string { return fmt.Sprintf("data-%s=%q", d.Name, d.Value) } // makeTag produces a function that makes tags of the given // type. func makeTag(name string) func(Attrs, ...toHTML) Tag { return func(attrs Attrs, children ...toHTML) Tag { return Tag{ Name: name, Attrs: attrs, Children: children, } } } var ( dd = makeTag("dd") dt = makeTag("dt") dl = makeTag("dl") details = makeTag("details") summary = makeTag("summary") span = makeTag("span") div = makeTag("div") ) ================================================ FILE: docs/book/utils/markerdocs/main.go ================================================ /* Copyright 2019 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 main import ( "encoding/json" "fmt" "log" "os" "os/exec" "strings" "sigs.k8s.io/kubebuilder/docs/book/utils/plugin" ) // argType produces HTML for describing an Argument's type. func argType(arg *Argument) toHTML { if arg.Type == "slice" { return span(optionalClasses{"optional": arg.Optional, "slice": true}, argType(arg.ItemType)) } return span(classes{"optional"}, Text(arg.Type)) } // maybeDetails returns HTML describing summary and // details if present, otherwise returning an empty fragment. func maybeDetails(help *DetailedHelp) toHTML { if help.Summary == "" && help.Details == "" { return Fragment{} } return Fragment{ details(nil, summary(optionalClasses{"no-details": help.Details == ""}, Text(help.Summary)), // NB(directxman12): if we don't wrap with newlines, markdown won't be parsed Text(wrapWithNewlines(help.Details)))} } // markerTemplate returns HTML describing the documentation for a given marker. func markerTemplate(marker *MarkerDoc) toHTML { // the marker name term := dt(classes{"literal", "name"}, Text("// +"+marker.Name)) // the args summary (displayed in summary mode) var fields []toHTML for _, field := range marker.Fields { fields = append(fields, Fragment{ dt(optionalClasses{"argument": true, "optional": field.Optional, "literal": true}, Text(field.Name)), dd(optionalClasses{"argument": true, "type": true, "optional": field.Optional}, argType(&field.Argument)), }) } argsDef := dd(classes{"args"}, dl(classes{"args", "summary"}, fields...)) // the argument details (displayed in details mode) var args Fragment for _, field := range marker.Fields { args = append(args, Fragment{ dt(optionalClasses{"argument": true, "optional": field.Optional, "literal": true}, Text(field.Name)), dd(optionalClasses{"argument": true, "type": true, "optional": field.Optional}, argType(&field.Argument)), dd(classes{"description"}, maybeDetails(&field.DetailedHelp))}) } // the help (displayed in both modes) helpDef := dd(classes{"description"}, maybeDetails(&marker.DetailedHelp), dl(classes{"args"}, args)) // the overall wrapping marker (common classes go here to make it easier to select // on certain things w/o duplication) markerAttrs := attrs{ optionalClasses{ "marker": true, "deprecated": marker.DeprecatedInFavorOf != nil, "anonymous": marker.Anonymous(), }, dataAttr{Name: "target", Value: marker.Target}, } if marker.DeprecatedInFavorOf != nil { markerAttrs = append(markerAttrs, dataAttr{Name: "deprecated", Value: *marker.DeprecatedInFavorOf}) } return div(markerAttrs, term, argsDef, helpDef) } // MarkerDocs is a plugin that autogenerates documentation // for markers known to controller-gen. Generated pages // will be added to locations marked `{{#markerdocs category name}}`. // This allows us to put additional documentation in each category. type MarkerDocs struct { // Args contains the arguments to pass to controller-gen to get // marker help JSON output. Each key is a prefix to apply to // category names (for disambiguation), and each value is an invocation // of controller-gen. Args map[string][]string } // SupportsOutput implements plugin.Plugin func (MarkerDocs) SupportsOutput(_ string) bool { return true } // Process implements plugin.Plugin func (p MarkerDocs) Process(input *plugin.Input) error { markerDocs, err := p.getMarkerDocs() if err != nil { return fmt.Errorf("unable to fetch marker docs: %v", err) } // first, find all categories... markersByCategory := make(map[string][]MarkerDoc) for _, cat := range markerDocs { if cat.Category == "" { // skip un-named categories, which are intended to be hidden // (e.g. all the per-generate idendical output rules) continue } markersByCategory[cat.Category] = cat.Markers } usedCategories := make(map[string]struct{}, len(markersByCategory)) // NB(directxman12): we use existing pages instead of generating new ones so that we can add additional // content to the pages (for instance, additional description of the category). // ...then, go through the book, finding all instances of `{{#markerdocs }}` and replacing them // with the appropriate docs ... err = plugin.EachCommand(&input.Book, "markerdocs", func(chapter *plugin.BookChapter, category string) (string, error) { category = strings.TrimSpace(category) markers, knownCategory := markersByCategory[category] if !knownCategory { return "", fmt.Errorf("unknown category %q", category) } // HTML5 says that any characters are valid in ID except for space, // but may not be empty (which we prevent by skipping un-named categories): // https://www.w3.org/TR/html52/dom.html#element-attrdef-global-id categoryAlias := strings.ReplaceAll(category, " ", "-") content := new(strings.Builder) // NB(directxman12): wrap this in a div to prevent the markdown processor from inserting extra paragraphs _, err := fmt.Fprintf(content, "
", categoryAlias) if err != nil { return "", fmt.Errorf("unable to render marker documentation summary: %v", err) } // write the markers for _, marker := range markers { if err := markerTemplate(&marker).WriteHTML(content); err != nil { return "", fmt.Errorf("unable to render documentation for marker %q: %v", marker.Name, err) } } if _, err = fmt.Fprintf(content, "
"); err != nil { return "", fmt.Errorf("unable to render marker documentation: %v", err) } usedCategories[category] = struct{}{} return content.String(), nil }) if err != nil { return err } // ... and finally make sure we didn't miss any if len(usedCategories) != len(markersByCategory) { unusedCategories := make([]string, 0, len(markersByCategory)-len(usedCategories)) for cat := range markersByCategory { if _, ok := usedCategories[cat]; !ok { unusedCategories = append(unusedCategories, cat) } } return fmt.Errorf("unused categories %v", unusedCategories) } return nil } // wrapWithNewlines ensures that we begin and end with a newline character. // this is important to ensure that markdown is parsed inside of details elements. func wrapWithNewlines(src string) string { if len(src) < 4 { return src } if src[0] != '\n' { src = "\n" + src } if src[1] != '\n' { src = "\n" + src } if src[len(src)-1] != '\n' { src = src + "\n" } if src[len(src)-2] != '\n' { src = src + "\n" } return src } // getMarkerDocs fetches marker documentation from controller-gen func (p MarkerDocs) getMarkerDocs() ([]CategoryDoc, error) { var res []CategoryDoc for categoryPrefix, args := range p.Args { cmd := exec.Command("controller-gen", args...) outRaw, err := cmd.Output() if err != nil { return nil, err } var invocationRes []CategoryDoc if err := json.Unmarshal(outRaw, &invocationRes); err != nil { return nil, err } for i, category := range invocationRes { // leave empty categories as-is, so that they're skipped if category.Category == "" { continue } invocationRes[i].Category = categoryPrefix + category.Category } res = append(res, invocationRes...) } return res, nil } func main() { if err := plugin.Run(MarkerDocs{ Args: map[string][]string{ // marker args "": {"-wwww", "crd", "webhook", "rbac:roleName=cheddar" /* role name doesn't mean anything here */, "object", "schemapatch:manifests=."}, // cli options args "CLI: ": {"-hhhh"}, }, }, os.Stdin, os.Stdout, os.Args[1:]...); err != nil { log.Fatal(err.Error()) } } ================================================ FILE: docs/book/utils/plugin/input.go ================================================ /* Copyright 2019 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 plugin import ( "bytes" "encoding/json" "fmt" ) // Context is a (partial) mdBook execution context. type Context struct { Root string `json:"root"` Config Config `json:"config"` } // Config is a (partial) mdBook config type Config struct { Book BookConfig `json:"book"` } // BookConfig is a (partial) mdBook [book] stanza type BookConfig struct { Src string `json:"src"` } // Book is an mdBook book. type Book struct { Items []BookItem `json:"items"` NonExhaustive *struct{} `json:"__non_exhaustive"` } // BookSection is an mdBook section. type BookSection struct { Items []BookItem `json:"items"` } // BookItem is an mdBook item. // It wraps an underlying struct to provide proper marshalling and unmarshalling // according to what serde produces/expects. type BookItem bookItem // UnmarshalJSON implements encoding/json.Unmarshaler func (b *BookItem) UnmarshalJSON(input []byte) error { // match how serde serializes rust enums. if input[0] == '"' { // actually a an empty variant var variant string if err := json.Unmarshal(input, &variant); err != nil { return err } switch variant { case "Separator": b.Separator = true default: return fmt.Errorf("unknown book item variant %s", variant) } return nil } item := bookItem(*b) if err := json.Unmarshal(input, &item); err != nil { return err } *b = BookItem(item) return nil } // MarshalJSON implements encoding/json.Marshaler func (b BookItem) MarshalJSON() ([]byte, error) { if b.Separator { return json.Marshal("Separator") } return json.Marshal(bookItem(b)) } // bookItem is the underlying mdBook item without custom serialization. type bookItem struct { Chapter *BookChapter `json:"Chapter"` Separator bool `json:"-"` } // BookChapter is an mdBook chapter. type BookChapter struct { Name string `json:"name"` Content string `json:"content"` Number SectionNumber `json:"number"` SubItems []BookItem `json:"sub_items"` Path string `json:"path"` ParentNames []string `json:"parent_names"` } // SectionNumber is an mdBook section number (e.g. `1.2` is `{1,2}`). type SectionNumber []uint32 // Input is the tuple that's presented to mdBook plugins. // It's deserialized from a slice `[context, book]`, matching // a Rust tuple. type Input struct { Context Context Book Book } // UnmarshalJSON implements encoding/json.Unmarshaler func (p *Input) UnmarshalJSON(input []byte) error { // deserialize from the JSON equivalent to the Rust tuple // `(context, book)` inputBuffer := bytes.NewBuffer(input) dec := json.NewDecoder(inputBuffer) tok, err := dec.Token() if err != nil { return err } if delim, isDelim := tok.(json.Delim); !isDelim || delim != '[' { return fmt.Errorf("expected [, got %s", tok) } if err := dec.Decode(&p.Context); err != nil { return err } if err := dec.Decode(&p.Book); err != nil { return err } tok, err = dec.Token() if err != nil { return err } if delim, isDelim := tok.(json.Delim); !isDelim || delim != ']' { return fmt.Errorf("expected ], got %s", tok) } return nil } // ChapterVisitor visits each BookChapter in a book, getting an actual // pointer to the chapter that it can modify. type ChapterVisitor func(*BookChapter) error // EachItem calls the given visitor for each chapter in the given item, // passing a pointer to the actual chapter that the visitor can modify. func EachItem(parentItem *BookItem, visitor ChapterVisitor) error { if parentItem.Chapter == nil { return nil } if err := visitor(parentItem.Chapter); err != nil { return err } // pass a pointer to the structure, not the iteration variable for i := range parentItem.Chapter.SubItems { if err := EachItem(&parentItem.Chapter.SubItems[i], visitor); err != nil { return err } } return nil } // EachItemInBook functions identically to EachItem, except that it visits // all chapters in the book. func EachItemInBook(book *Book, visitor ChapterVisitor) error { // pass a pointer to the structure, not the iteration variable for i := range book.Items { if err := EachItem(&book.Items[i], visitor); err != nil { return err } } return nil } ================================================ FILE: docs/book/utils/plugin/plugin.go ================================================ /* Copyright 2019 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 plugin import ( "encoding/json" "fmt" "io" "os" ) // Plugin represents a mdBook plugin. type Plugin interface { // SupportsOutput checks if the given plugin supports the given output format. SupportsOutput(string) bool // Process modifies the book in the input, which gets returned as the result of the plugin. Process(*Input) error } // Run runs the given plugin on the given input stream, outputting its result to the given // result, assuming the given command-line args (without program name). func Run(plug Plugin, inputRaw io.Reader, outputRaw io.Writer, args ...string) error { if len(args) > 1 && args[0] == "supports" { // we support any renderer, no need to check (name is in Args[1]) if plug.SupportsOutput(args[1]) { return nil } return fmt.Errorf("output format %q not supported", args[1]) } var input Input dec := json.NewDecoder(inputRaw) if err := dec.Decode(&input); err != nil { return fmt.Errorf("unable to decode preprocessor input: %v", err) } if err := plug.Process(&input); err != nil { return err } out, err := json.Marshal(&input.Book) if err != nil { return fmt.Errorf("unable to encode output book object: %v", err) } if n, err := outputRaw.Write(out); err != nil || n < len(out) { if err == nil && n < len(out) { err = io.ErrShortWrite } return fmt.Errorf("unable to write output book object: %v", err) } if err = os.WriteFile("/tmp/litout.json", out, os.ModePerm); err != nil { return fmt.Errorf("unable to write output book object: %v", err) } return nil } ================================================ FILE: docs/book/utils/plugin/utils.go ================================================ /* Copyright 2019 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 plugin import ( "fmt" "strings" ) // EachCommand looks for mustache-like declarations of the form `{{#cmd args}}` // in each book chapter, and calls the callback for each one, substituting it // for the result. func EachCommand(book *Book, cmd string, callback func(chapter *BookChapter, args string) (string, error)) error { cmdStart := fmt.Sprintf("{{#%s ", cmd) return EachItemInBook(book, func(chapter *BookChapter) error { if chapter.Content == "" { return nil } // figure out all the trigger expressions partsRaw := strings.Split(chapter.Content, cmdStart) // the first section won't start with `{{# ` as per how split works if len(partsRaw) < 2 { return nil } var res []string res = append(res, partsRaw[0]) for _, part := range partsRaw[1:] { endDelim := strings.Index(part, "}}") if endDelim < 0 { return fmt.Errorf("missing end delimiter in chapter %q", chapter.Name) } newContents, err := callback(chapter, part[:endDelim]) if err != nil { return err } res = append(res, newContents) res = append(res, part[endDelim+2:]) } chapter.Content = strings.Join(res, "") return nil }) } ================================================ FILE: docs/kubebuilder_annotation.md ================================================ # Kubebuilder Annotation If you have been using Kubebuilder, you must have seen comments such as `// +kubebuilder:rbac: ....` , `// +kubebuilder:resource:...` in the scaffolder Go files. These special comments are used by kubebuilder tools (controller tools) to generate CRD, RBAC, and webhook manifests. In kubebuilder, these special comments are `Kubebuilder Annotation`, a.k.a `annotation`. It is designed for this kind of use case: To use kubebuilder tools, all you have to do is focus on writing your code, and put instructions with parameters as annotations along with your code, so that everything will be handled based on these annotations instructions by kubebuilder. This document illustrates the syntax of these annotations. ## Kubebuilder Annotation Syntax Kubebuilder Annotation has a series of tokens separated by colons into groups from left to right. Each **Token** is a string identifier in an annotation instance. It has meaning by its position in a token slice, in the form of **+[header]:[module]:[submodule]:[key-value elements]** Go Annotation starts with `+` (e.g. `// +kubebuilder`) to differentiate from regular go comments. ## Token types - **header** is the identifier of a group of annotations. It helps the user know which project provides this annotation. For example, in the Kubernetes project, headers like `kubebuilder`, `k8s`, `genclient`, etc. are all project identifiers. A header is required for all annotations, since you may use multiple annotations from different projects in the same codebase. - **module** is the identifier of a functional module in an annotation. An annotation may have a group of modules, each of which performs a particular function. - **submodule** (optional) In some cases, the module has a big functional scope, split into fine-grained sub-modules, which provide the flexibility of extending module functionality. For example: **module:submodule1:submodule2:submodule3** submodule can be multiple following one by one. ## Levels of symbols Delimiter symbols are distinguished to work in different levels from top-down for splitting values string in tokens, which provides readability and efficiency. - **Colon** Colon `:` is the 1st level delimiter (to annotation) only for separate tokens. Tokens on different sides of the colon should refer to different token types. - **Comma** Comma `,` is the 2nd level delimiter (to annotation) for splitting key-value pairs in **key-value elements** which is normally the last token in the annotation. e.g. `+kubebuilder:printcolumn:name=,type=,description=,JSONPath:<.spec.Name>,priority=,format=` It works within token which is the 2nd level of annotation, so it is called "2nd level delimiter" - **Equal sign** Equal sign `=` is the 3rd level delimiter (to annotation) for identifying key and value. Since the `key=value` parts are split from single token (2nd level), its inner delimiter `=` works for next level (3rd level) - **Semicolon sign** Semicolon sign `;` is the 4th level delimiter, which works on the `value` part (4th level) of `key=value`(3rd level) for splitting individual values. e.g. `key1=value1;value2;value3` - **Pipe sign or Vertical bar** Pipe sign `|` is the 5th level delimiter, which works inside the single `value` part (4th level) indicating key and value in case of the single value has nested key-value structure. e.g. `outerkey=innerkey1|innervalue1` ### Examples #### Webhook annotation examples **[header]** is `kubebuilder`, **[module]** is `webhook`, **[submodule]** is `admission` or `serveroption` ```golang // +kubebuilder:webhook:admission:groups=apps,resources=deployments,verbs=CREATE;UPDATE,name=bar-webhook,path=/bar,type=mutating,failure-policy=Fail // +kubebuilder:webhook:serveroption:port=7890,cert-dir=/tmp/test-cert,service=test-system|webhook-service,selector=app|webhook-server,secret=test-system|webhook-secret,mutating-webhook-config-name=test-mutating-webhook-cfg,validating-webhook-config-name=test-validating-webhook-cfg ``` **Notes:** 1. Separate two `submodule` (categories) under `webhook`: 1) `admission`and 2) `serveroption`, handling webhookTags and serverTags separately. 2. For each submodule, all key values should put in the same comment line. 3. using `|` for splitting key value of `lables` #### RBAC Annotation examples **[header]** is `kubebuilder` **[module]** is `rbac` No submodule at this moment, support annotations like: `// +rbac`, `// +kubebuilder:rbac` ```golang // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;delete // +rbac:groups=apps,resources=deployments,verbs=get;list;watch;delete ``` ================================================ FILE: docs/kubebuilder_v0_v1_difference.md ================================================ # Kubebuilder v0 vs. v1 Kubebuilder 1.0 adds a new flag `--project-version`, it accepts two different values, `v0` and `v1`. When `v0` is used, the kubebuilder behavior and workflow are the same as kubebuilder 0.*. When `v1` is specified, the generated v1 project layout is architecturally different from v0 project. v1 project use [controller-runtime](https://github.com/kubernetes-sigs/controller-runtime) set of libraries for controller implementation and used tools under [controller-tools](https://github.com/kubernetes-sigs/controller-tools) for scaffolding and generation. ## Command difference - kubebuilder v0 has `init`, `create controller`, `create resource`, `create config`, `generate` commands and the workflow is: ``` kubebuilder init --domain example.com kubebuilder create resource --group --version --kind GOBIN=${PWD}/bin go install ${PWD#$GOPATH/src/}/cmd/controller-manager bin/controller-manager --kubeconfig ~/.kube/config kubectl apply -f hack/sample/.yaml docker build -f Dockerfile.controller . -t docker push kubebuilder create config --controller-image --name kubectl apply -f hack/install.yaml ``` Every time the resource or controller is updated, users need to run `kubebuilder generate` to regenerate the project. - kubebuilder v1 has `init`, `create api` commands and the workflow is ``` kubebuilder init --domain example.com --license apache2 --owner "The Kubernetes authors" kubebuilder create api --group ship --version v1beta1 --kind Frigate make install make run ``` In a v1 project, there is no generate command. When the resource or controller is updated, users don't need to regenerate the project. ## Scaffolding difference - v0 project contains a directory `pkg/client` while v1 project doesn't - v0 project contains a directory `inject` while v1 project doesn't - v0 project layout follows predefined directory layout `pkg/apis` and `pkg/controller` while v1 project accepts user-specified path - In v1 project, there is a `init()` function for every api and controller. ## Library difference ### Controller libraries - v0 projects import the controller library from kubebuilder `kubebuilder/pkg/controller`. It provides a `GenericController` type with a list of functions. - v1 projects import the controller libraries from controller-runtime, such as `controller-runtime/pkg/controller`, `controller-runtime/pkg/reconcile`. ### Client libraries - In v0 projects, the client libraries is generated by `kubebuilder generate` under directory `pkg/client` and imported wherever they are used in the project. - v1 projects import the dynamic client library from controller-runtime `controller-runtime/pkg/client`. ## Wiring difference Wiring refers to the mechanics of integrating controllers into controller-managers and injecting the dependencies in them. - v0 projects have an `inject` package and it provides functions for adding the controller to controller-manager as well as registering CRDs. - v1 projects don't have a `inject` package, the controller is added to controller-manager by a `init` function inside add_.go file inside the controller directory. The types are registered by an `init` function inside _types.go file inside the apis directory. ================================================ FILE: docs/migration_guide.md ================================================ # Migration guide from v0 project to v1 project This document describes how to migrate a project created by kubebuilder v0 to a project created by kubebuilder v1. Before jumping into the detailed instructions, please take a look at the list of [major differences between kubebuilder v0 and kubebuilder v1](kubebuilder_v0_v1_difference.md). The recommended way of migrating a v0 project to a v1 project is to create a new v1 project and copy/modify the code from v0 project to it. ## Init a v1 project Find the project's domain name from the old project's pkg/apis/doc.go and use it to initiate a new project with `kubebuilder init --project-version v1 --domain ` ## Create api Find the group/version/kind names from the project's pkg/apis. The group and version names are directory names while the kind name can be found from *_types.go. Note that the kind name should be capitalized. Create api in the new project with `kubebuilder create api --group --version --kind ` If there are several resources in the old project, repeat the `kubebuilder create api` command to create all of them. ## Copy types.go Copy the content of `_types.go` from the old project into the file `_types.go` in the new project. Note that in the v1 project, there is a section containing `List` and `init` functions. Please keep this section. ``` // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +genclient:nonNamespaced // HelloList contains a list of Hello type HelloList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` Items []Hello `json:"items"` } func init() { SchemeBuilder.Register(&Hello{}, &HelloList{}) } ``` ## Copy and modify controller code ### copy and update reconcile function Note that in v0 and v1 projects, the `Reconcile` functions have different arguments and return types. - `Reconcile` function in v0 project: `func (bc *Controller) Reconcile(k types.ReconcileKey) error` - `Reconcile` function in v1 project: `func (r *Reconcile) Reconcile(request reconcile.Request) (reconcile.Result, error)` Remove the original body of `Reconcile` function inside the v1 project and copy the body of the `Reconcile` function from the v0 project to the v1 project. Then apply following changes: - add `reconcile.Result{}` as the first value in every `return` statement - change the call of client functions such as `Get`, `Create`, `Update`. In v0 projects, the call of client functions has the format like `bc.Lister.().Get()` or `bc.KubernetesClientSet....Get()`. They can be replaced by `r.Client` functions. Here are several examples of updating the client function from v0 project to v1 project: ``` # in v0 project mc, err := bc.memcachedLister.Memcacheds(k.Namespace).Get(k.Name) # in v1 project, change to mc := &myappsv1alpha1.Memcached{} err := r.Client.Get(context.TODO(), request.NamespacedName, mc) # in v0 project dp, err := bc.KubernetesInformers.Apps().V1().Deployments().Lister().Deployments(mc.Namespace).Get(mc.Name) # in v1 project, change to dp := &appsv1.Deployment{} err := r.Client.Get(context.TODO(), request.NamespacedName, dp) dep := &appsv1.Deployment{...} # in v0 project dp, err := bc.KubernetesClientSet.AppsV1().Deployments(mc.Namespace).Create(dep) # in v1 project, change to err := r.Client.Create(context.TODO(), dep) dep := &appsv1.Deployment{...} # in v0 project dp, err = bc.KubernetesClientSet.AppsV1().Deployments(mc.Namespace).Update(deploymentForMemcached(mc)) # in v1 project, change to err := r.Client.Update(context.TODO(), dep) labelSelector := labels.SelectorFrom{...} # in v0 project pods, err := bc.KubernetesInformers.Core().V1().Pods().Lister().Pods(mc.Namespace).List(labelSelector) # in v1 project, change to pods := &v1.PodList{} err = r.Client.List(context.TODO(), &client.ListOptions{LabelSelector: labelSelector}, pods) ``` - add library imports used in the v0 project to v1 project such as log, fmt or k8s libraries. Note that libraries from kubebuilder or from the old project's client package shouldn't be added. ### update add function In a v0 project controller file, there is a `ProvideController` function creating a controller and adding some watches. In v1 projects, the corresponding function is `add`. For this part, you don't need to copy any code from the v0 project to v1 project. You need to add some watchers in the v1 project's `add` function based on what `watch` functions are called in the v0 project's `ProvideController` function. Here are several examples: ``` gc := &controller.GenericController{...} gc.Watch(&myappsv1alpha1.Memcached{}) gc.WatchControllerOf(&v1.Pod{}, eventhandlers.Path{bc.LookupRS, bc.LookupDeployment, bc.LookupMemcached}) ``` need to be changed to: ``` c, err := controller.New{...} c.Watch(&source.Kind{Type: &myappsv1alpha1.Memcached{}}, &handler.EnqueueRequestForObject{}) c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{ IsController: true, OwnerType: &myappsv1alpha1.Memcached{}, }) ``` ### copy other functions If the `reconcile` function depends on some other user-defined functions, copy those functions as well into the v1 project. ## Copy user libraries If there are some user-defined libraries in the old project, make sure to copy them as well into the new project. ## Update dependency Open the Gopkg.toml file in the old project and find if there is user-defined dependency in this block: ``` # Users add deps lines here [prune] go-tests = true #unused-packages = true # Note: Stanzas below are generated by Kubebuilder and may be rewritten when # upgrading kubebuilder versions. # DO NOT MODIFY BELOW THIS LINE. ``` Copy those dependencies into the new project's Gopkg.toml file **before** the line ``` # STANZAS BELOW ARE GENERATED AND MAY BE WRITTEN - DO NOT MODIFY BELOW THIS LINE. ``` ## Copy other user files If there are other user-created files in the old project, such as any build scripts, or README.md files. Copy those files into the new project. ## Confirmation Run `make` to make sure the new project can be built and pass all the tests. Run `make install` and `make run` to make sure the api and controller work well on cluster. ================================================ FILE: docs/testing/e2e.md ================================================ **Running End-to-end Tests on Remote Clusters** **This document is for kubebuilder v1 only** This article outlines steps to run e2e tests on remote clusters for controllers created using `kubebuilder`. For example, after developing a database controller, the developer may want to run some e2e tests on a GKE cluster to verify the controller is working as expected. Currently, `kubebuilder` does not provide a template for running the e2e tests. This article serves to address this deficit. The steps are as follows: 1. Create a test file named `_test.go` populated with template below (referring [this](https://github.com/foxish/application/blob/master/e2e/main_test.go)): ``` import ( "k8s.io/client-go/tools/clientcmd" clientset "k8s.io/redis-operator/pkg/client/clientset/versioned/typed//" ...... ) // Specify kubeconfig file func getClientConfig() (*rest.Config, error) { return clientcmd.BuildConfigFromFlags("", path.Join(os.Getenv("HOME"), "")) } // Set up test environment var _ = Describe(" should work", func() { config, err := getClientConfig() if err != nil { ...... } // Construct kubernetes client k8sClient, err := kubernetes.NewForConfig(config) if err != nil { ...... } // Construct controller client client, err := clientset.NewForConfig(config) if err != nil { ...... } BeforeEach(func() { // Create environment-specific resources such as controller image StatefulSet, // CRDs etc. Note: refer "install.yaml" created via "kubebuilder create config" // command to have an idea of what resources to be created. ...... }) AfterEach(func() { // Delete all test-specific resources ...... // Delete all environment-specific resources ...... }) // Declare a list of testing specifications with corresponding test functions // Note: test-specific resources are normally created within the test functions It("should do something", func() { testDoSomething(k8sClient, roClient) }) ...... ``` 2. Write some controller-specific e2e tests 3. Build controller image and upload it to an image storage website such as [gcr.io](https://cloud.google.com/container-registry/) 4. `go test ` ================================================ FILE: docs/testing/integration.md ================================================ **Writing and Running Integration Tests** **This document is for kubebuilder v1 only** This article explores steps to write and run integration tests for controllers created using Kubebuilder. Kubebuilder provides a template for writing integration tests. You can simply run all integration (and unit) tests within the project by running: `make test` For example, there is a controller watching *Parent* objects. The *Parent* objects create *Child* objects. Note that the *Child* objects must have their `.ownerReferences` field setting to the `Parent` objects. You can find the template under `pkg/controllers/parent/parent_controller_test.go`: ``` package parent import ( _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" childapis "k8s.io/child/pkg/apis" childv1alpha1 "k8s.io/childrepo/pkg/apis/child/v1alpha1" parentapis "k8s.io/parent/pkg/apis" parentv1alpha1 "k8s.io/parentrepo/pkg/apis/parent/v1alpha1" ...... ) const timeout = time.Second * 5 var c client.Client var expectedRequest = reconcile.Request{NamespacedName: types.NamespacedName{Name: "parent", Namespace: "default"}} var childKey = types.NamespacedName{Name: "child", Namespace: "default"} func TestReconcile(t *testing.T) { g := gomega.NewGomegaWithT(t) // Parent instance to be created. parent := &parentv1alpha1.Parent{ ObjectMeta: metav1.ObjectMeta{ Name: "parent", Namespace: "default", }, Spec: metav1.ParentSpec{ SomeSpecField: "SomeSpecValue", AnotherSpecField: "AnotherSpecValue", }, } // Setup the Manager and Controller. Wrap the Controller Reconcile function // so it writes each request to a channel when it is finished. mgr, err := manager.New(cfg, manager.Options{}) // Setup Scheme for all resources. if err = parentapis.AddToScheme(mgr.GetScheme()); err != nil { t.Logf("failed to add Parent scheme: %v", err) } if err = childapis.AddToScheme(mgr.GetScheme()); err != nil { t.Logf("failed to add Child scheme: %v", err) } // Set up and start test manager. reconciler, err := newReconciler(mgr) g.Expect(err).NotTo(gomega.HaveOccurred()) recFn, requests := SetupTestReconcile(reconciler) g.Expect(add(mgr, recFn)).NotTo(gomega.HaveOccurred()) defer close(StartTestManager(mgr, g)) // Create the Parent object and expect the Reconcile and Child to be created. c = mgr.GetClient() err = c.Create(context.TODO(), parent) g.Expect(err).NotTo(gomega.HaveOccurred()) defer c.Delete(context.TODO(), parent) g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedRequest))) // Verify Child is created. child := &childv1alpha1.Child{} g.Eventually(func() error { return c.Get(context.TODO(), childKey, child) }, timeout). Should(gomega.Succeed()) // Manually delete Child since GC isn't enabled in the test control plane. g.Expect(c.Delete(context.TODO(), child)).To(gomega.Succeed()) } ``` `SetupTestReconcile` function above brings up an API server and etcd instance. Note that there are no nodes created for the integration testing environment. If you want to test your controller on a real node, you should write end-to-end tests. The manager is started as part of the test itself (`StartTestManager` function). Both functions are located in `pkg/controllers/parent/parent_controller_suite_test.go` file. The file also contains a `TestMain` function that allows you to specify CRD directory paths for the testing environment. ================================================ FILE: docs/windows.md ================================================ # Windows Support Since no efforts have been made to add support for Windows in the past couple of years, we have decided not to pursue native Windows support at this time, considering both the additional maintenance overhead it adds to the project and the limited community contributions in that area. That said, it’s still possible to use and contribute to Kubebuilder on a Windows machine by using WSL2 (Windows Subsystem for Linux) together with Docker Desktop. - Learn more about setting up WSL2 in the [official docs](https://learn.microsoft.com/en-us/windows/wsl/). - The [Docker Desktop documentation](https://docs.docker.com/desktop/features/wsl/) has instructions on how to set up Docker to use WSL2 as the backend on Windows. - You can also learn more about setting up kind with Docker on WSL2 in the [kind official documentation](https://kind.sigs.k8s.io/docs/user/using-wsl2/). All other dependencies and environment settings can be set up by following the Linux instructions. ================================================ FILE: go.mod ================================================ module sigs.k8s.io/kubebuilder/v4 go 1.25.3 retract v4.10.0 // invalid filename causes go get/install failure (#5211) require ( github.com/gobuffalo/flect v1.0.3 github.com/h2non/gock v1.2.0 github.com/onsi/ginkgo/v2 v2.28.1 github.com/onsi/gomega v1.39.1 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 go.yaml.in/yaml/v3 v3.0.4 golang.org/x/mod v0.34.0 golang.org/x/text v0.35.0 golang.org/x/tools v0.43.0 helm.sh/helm/v3 v3.20.1 k8s.io/apimachinery v0.35.2 sigs.k8s.io/yaml v1.6.0 ) require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // 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/pkg/errors v0.9.1 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.52.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/inf.v0 v0.9.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // 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 ) ================================================ FILE: go.sum ================================================ github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 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/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/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/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= 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-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/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE= github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw= github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= 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/kr/pretty v0.2.1/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/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= 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/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/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/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/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 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/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= 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/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= 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= 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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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.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= helm.sh/helm/v3 v3.20.1 h1:T8PodUaH1UwNvE+imUA2mIKjJItY8g7CVvLVP5g4NzI= helm.sh/helm/v3 v3.20.1/go.mod h1:Fl1kBaWCpkUrM6IYXPjQ3bdZQfFrogKArqptvueZ6Ww= k8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8= k8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 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/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.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= ================================================ FILE: hack/docs/check.sh ================================================ #!/usr/bin/env bash # Copyright 2023 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. check_directory="$(dirname "$0")/../../docs/book/src/" # Check docs directory first. If there are any uncommitted change, fail the test. if [[ $(git status ${check_directory} --porcelain) ]]; then echo "Generate Docs test precondition failed!" echo "Please commit the change under docs directory before running the Generate Docs test" exit 1 fi $(dirname "$0")/generate.sh # Check if there are any changes to files under testdata directory. if [[ $(git status ${check_directory} --porcelain) ]]; then git status ${check_directory} --porcelain git diff ${check_directory} echo "Generate Docs failed!" echo "Please, if you have changed the scaffolding make sure you have run: make generate" exit 1 else echo "Generate Docs passed!" fi ================================================ FILE: hack/docs/generate.sh ================================================ #!/usr/bin/env bash # Copyright 2023 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. source "$(dirname "$0")/../../test/common.sh" build_kb # ensure that destroy succeed chmod -R +w docs/book/src/cronjob-tutorial/testdata/project/ chmod -R +w docs/book/src/getting-started/testdata/project/ docs_gen_directory="$(dirname "$0")/../../hack/docs/generate_samples.go" go run ${docs_gen_directory} ================================================ FILE: hack/docs/generate_samples.go ================================================ /* Copyright 2023 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 main import ( "log/slog" "os" cronjob "sigs.k8s.io/kubebuilder/v4/hack/docs/internal/cronjob-tutorial" gettingstarted "sigs.k8s.io/kubebuilder/v4/hack/docs/internal/getting-started" multiversion "sigs.k8s.io/kubebuilder/v4/hack/docs/internal/multiversion-tutorial" "sigs.k8s.io/kubebuilder/v4/internal/logging" ) // KubebuilderBinName make sure executing `build_kb` to generate kb executable from the source code const KubebuilderBinName = "/tmp/kubebuilder/bin/kubebuilder" type tutorialGenerator interface { Prepare() GenerateSampleProject() UpdateTutorial() CodeGen() } func main() { type generator func() tutorials := map[string]generator{ "cronjob": updateCronjobTutorial, "getting-started": updateGettingStarted, "multiversion": updateMultiversionTutorial, } opts := logging.HandlerOptions{ SlogOpts: slog.HandlerOptions{ Level: slog.LevelInfo, }, } handler := logging.NewHandler(os.Stdout, opts) logger := slog.New(handler) slog.SetDefault(logger) slog.Info("Generating documents...") for tutorial, updater := range tutorials { slog.Info("Generating tutorial", "name", tutorial) updater() } } func updateTutorial(generator tutorialGenerator) { generator.Prepare() generator.GenerateSampleProject() generator.UpdateTutorial() generator.CodeGen() } func updateCronjobTutorial() { samplePath := "docs/book/src/cronjob-tutorial/testdata/project/" sp := cronjob.NewSample(KubebuilderBinName, samplePath) updateTutorial(&sp) } func updateGettingStarted() { samplePath := "docs/book/src/getting-started/testdata/project" sp := gettingstarted.NewSample(KubebuilderBinName, samplePath) updateTutorial(&sp) } func updateMultiversionTutorial() { samplePath := "docs/book/src/multiversion-tutorial/testdata/project" sp := cronjob.NewSample(KubebuilderBinName, samplePath) updateTutorial(&sp) multi := multiversion.NewSample(KubebuilderBinName, samplePath) updateTutorial(&multi) } ================================================ FILE: hack/docs/internal/cronjob-tutorial/api_design.go ================================================ /* Copyright 2023 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 cronjob const cronjobSpecExplaination = ` // +kubebuilder:docs-gen:collapse=Imports /* First, let's take a look at our spec. As we discussed before, spec holds *desired state*, so any "inputs" to our controller go here. Fundamentally a CronJob needs the following pieces: - A schedule (the *cron* in CronJob) - A template for the Job to run (the *job* in CronJob) We'll also want a few extras, which will make our users' lives easier: - A deadline for starting jobs (if we miss this deadline, we'll just wait till the next scheduled time) - What to do if multiple jobs would run at once (do we wait? stop the old one? run both?) - A way to pause the running of a CronJob, in case something's wrong with it - Limits on old job history Remember, since we never read our own status, we need to have some other way to keep track of whether a job has run. We can use at least one old job to do this. We'll use several markers (` + "`" + `// +comment` + "`" + `) to specify additional metadata. These will be used by [controller-tools](https://github.com/kubernetes-sigs/controller-tools) when generating our CRD manifest. As we'll see in a bit, controller-tools will also use GoDoc to form descriptions for the fields. */ ` const cronjobSpecStruct = ` // schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. // +kubebuilder:validation:MinLength=0 // +required Schedule string` + " `" + `json:"schedule"` + "`" + ` // startingDeadlineSeconds defines in seconds for starting the job if it misses scheduled // time for any reason. Missed jobs executions will be counted as failed ones. // +optional // +kubebuilder:validation:Minimum=0 StartingDeadlineSeconds *int64` + " `" + `json:"startingDeadlineSeconds,omitempty"` + "`" + ` // concurrencyPolicy specifies how to treat concurrent executions of a Job. // Valid values are: // - "Allow" (default): allows CronJobs to run concurrently; // - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; // - "Replace": cancels currently running job and replaces it with a new one // +optional // +kubebuilder:default:=Allow ConcurrencyPolicy ConcurrencyPolicy` + " `" + `json:"concurrencyPolicy,omitempty"` + "`" + ` // suspend tells the controller to suspend subsequent executions, it does // not apply to already started executions. Defaults to false. // +optional Suspend *bool` + " `" + `json:"suspend,omitempty"` + "`" + ` // jobTemplate defines the job that will be created when executing a CronJob. // +required JobTemplate batchv1.JobTemplateSpec` + " `" + `json:"jobTemplate"` + "`" + ` // successfulJobsHistoryLimit defines the number of successful finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 SuccessfulJobsHistoryLimit *int32` + " `" + `json:"successfulJobsHistoryLimit,omitempty"` + "`" + ` // failedJobsHistoryLimit defines the number of failed finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 FailedJobsHistoryLimit *int32` + " `" + `json:"failedJobsHistoryLimit,omitempty"` + "`" + ` } /* We define a custom type to hold our concurrency policy. It's actually just a string under the hood, but the type gives extra documentation, and allows us to attach validation on the type instead of the field, making the validation more easily reusable. */ // ConcurrencyPolicy describes how the job will be handled. // Only one of the following concurrent policies may be specified. // If none of the following policies is specified, the default one // is AllowConcurrent. // +kubebuilder:validation:Enum=Allow;Forbid;Replace type ConcurrencyPolicy string const ( // AllowConcurrent allows CronJobs to run concurrently. AllowConcurrent ConcurrencyPolicy = "Allow" // ForbidConcurrent forbids concurrent runs, skipping next run if previous // hasn't finished yet. ForbidConcurrent ConcurrencyPolicy = "Forbid" // ReplaceConcurrent cancels currently running job and replaces it with a new one. ReplaceConcurrent ConcurrencyPolicy = "Replace" ) /* Next, let's design our status, which holds observed state. It contains any information we want users or other controllers to be able to easily obtain. We'll keep a list of actively running jobs, as well as the last time that we successfully ran our job. Notice that we use` + " `" + `metav1.Time` + "`" + ` instead of` + " `" + `time.Time` + "`" + ` to get the stable serialization, as mentioned above. */` const cronjobList = ` // active defines a list of pointers to currently running jobs. // +optional // +listType=atomic // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 Active []corev1.ObjectReference` + " `" + `json:"active,omitempty"` + "`" + ` // lastScheduleTime defines when was the last time the job was successfully scheduled. // +optional LastScheduleTime *metav1.Time` + " `" + `json:"lastScheduleTime,omitempty"` + "`" + ` ` const docCommentStatusSub = ` /* Finally, we have the rest of the boilerplate that we've already discussed. As previously noted, we don't need to change this, except to mark that we want a status subresource, so that we behave like built-in kubernetes types. */ // +kubebuilder:object:root=true // +kubebuilder:subresource:status ` ================================================ FILE: hack/docs/internal/cronjob-tutorial/controller_implementation.go ================================================ /* Copyright 2023 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 cronjob const controllerIntro = ` // +kubebuilder:docs-gen:collapse=Apache License /* We'll start out with some imports. You'll see below that we'll need a few more imports than those scaffolded for us. We'll talk about each one when we use it. */` const controllerImport = `import ( "context" "fmt" "maps" "slices" "time" "github.com/robfig/cron" kbatch "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" 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" ref "k8s.io/client-go/tools/reference" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" batchv1 "tutorial.kubebuilder.io/project/api/v1" ) /* Next, we'll need a Clock, which will allow us to fake timing in our tests. */ ` const controllerMockClock = ` /* We'll mock out the clock to make it easier to jump around in time while testing, the "real" clock just calls` + " `" + `time.Now` + "`" + `. */ type realClock struct{} func (_ realClock) Now() time.Time { return time.Now() } //nolint:staticcheck // Clock knows how to get the current time. // It can be used to fake out timing for testing. type Clock interface { Now() time.Time } // +kubebuilder:docs-gen:collapse=Clock Code Implementation // Definitions to manage status conditions const ( // typeAvailableCronJob represents the status of the CronJob reconciliation typeAvailableCronJob = "Available" // typeProgressingCronJob represents the status used when the CronJob is being reconciled typeProgressingCronJob = "Progressing" // typeDegradedCronJob represents the status used when the CronJob has encountered an error typeDegradedCronJob = "Degraded" ) /* Notice that we need a few more RBAC permissions -- since we're creating and managing jobs now, we'll need permissions for those, which means adding a couple more [markers](/reference/markers/rbac.md). */ ` const controllerReconcile = ` // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get /* Now, we get to the heart of the controller -- the reconciler logic. */ var ( scheduledTimeAnnotation = "batch.tutorial.kubebuilder.io/scheduled-at" ) ` const skipGoCycloLint = ` // nolint:gocyclo` const controllerReconcileLogic = `log := logf.FromContext(ctx) /* ### 1: Load the CronJob by name We'll fetch the CronJob using our client. All client methods take a context (to allow for cancellation) as their first argument, and the object in question as their last. Get is a bit special, in that it takes a [` + "`" + `NamespacedName` + "`" + `](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client?tab=doc#ObjectKey) as the middle argument (most don't have a middle argument, as we'll see below). Many client methods also take variadic options at the end. */ var cronJob batchv1.CronJob if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { if apierrors.IsNotFound(err) { // If the custom resource is not found then it usually means that it was deleted or not created // In this way, we will stop the reconciliation log.Info("CronJob resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get CronJob") return ctrl.Result{}, err } // Initialize status conditions if not yet present if len(cronJob.Status.Conditions) == 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation", }) if err := r.Status().Update(ctx, &cronJob); err != nil { log.Error(err, "Failed to update CronJob status") return ctrl.Result{}, err } /* After updating the status, we re-fetch the CronJob to ensure we are working with the latest version of the object from the API server. Kubernetes uses optimistic concurrency, meaning that any update (including a status update) may change the resource version. If we continue reconciliation with a stale copy, subsequent updates may fail with a conflict such as: "the object has been modified; please apply your changes to the latest version and try again". By re-fetching here, we keep our reconciliation logic in sync with the actual cluster state and avoid unnecessary conflicts and requeues. */ if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil { log.Error(err, "Failed to re-fetch CronJob") return ctrl.Result{}, err } } /* ### 2: List all active jobs, and update the status To fully update our status, we'll need to list all child jobs in this namespace that belong to this CronJob. Similarly to Get, we can use the List method to list the child jobs. Notice that we use variadic options to set the namespace and field match (which is actually an index lookup that we set up below). */ var childJobs kbatch.JobList if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: req.Name}); err != nil { log.Error(err, "unable to list child Jobs") /* Before updating, ensure we have the latest state of the resource to avoid conflict errors (e.g. "the object has been modified") that would re-trigger the reconcile loop. */ if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "ReconciliationError", Message: fmt.Sprintf("Failed to list child jobs: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return ctrl.Result{}, err } /* Once we have all the jobs we own, we'll split them into active, successful, and failed jobs, keeping track of the most recent run so that we can record it in status. Remember, status should be able to be reconstituted from the state of the world, so it's generally not a good idea to read from the status of the root object. Instead, you should reconstruct it every run. That's what we'll do here. We can check if a job is "finished" and whether it succeeded or failed using status conditions. We'll put that logic in a helper to make our code cleaner. */ // find the active list of jobs var activeJobs []*kbatch.Job var successfulJobs []*kbatch.Job var failedJobs []*kbatch.Job var mostRecentTime *time.Time // find the last run so we can update the status /* We consider a job "finished" if it has a "Complete" or "Failed" condition marked as true. Status conditions allow us to add extensible status information to our objects that other humans and controllers can examine to check things like completion and health. */ isJobFinished := func(job *kbatch.Job) (bool, kbatch.JobConditionType) { for _, c := range job.Status.Conditions { if (c.Type == kbatch.JobComplete || c.Type == kbatch.JobFailed) && c.Status == corev1.ConditionTrue { return true, c.Type } } return false, "" } // +kubebuilder:docs-gen:collapse=isJobFinished /* We'll use a helper to extract the scheduled time from the annotation that we added during job creation. */ getScheduledTimeForJob := func(job *kbatch.Job) (*time.Time, error) { timeRaw := job.Annotations[scheduledTimeAnnotation] if len(timeRaw) == 0 { return nil, nil } timeParsed, err := time.Parse(time.RFC3339, timeRaw) if err != nil { return nil, err } return &timeParsed, nil } // +kubebuilder:docs-gen:collapse=getScheduledTimeForJob for i, job := range childJobs.Items { _, finishedType := isJobFinished(&job) switch finishedType { case "": // ongoing activeJobs = append(activeJobs, &childJobs.Items[i]) case kbatch.JobFailed: failedJobs = append(failedJobs, &childJobs.Items[i]) case kbatch.JobComplete: successfulJobs = append(successfulJobs, &childJobs.Items[i]) } // We'll store the launch time in an annotation, so we'll reconstitute that from // the active jobs themselves. scheduledTimeForJob, err := getScheduledTimeForJob(&job) if err != nil { log.Error(err, "unable to parse schedule time for child job", "job", &job) continue } if scheduledTimeForJob != nil { if mostRecentTime == nil || mostRecentTime.Before(*scheduledTimeForJob) { mostRecentTime = scheduledTimeForJob } } } if mostRecentTime != nil { cronJob.Status.LastScheduleTime = &metav1.Time{Time: *mostRecentTime} } else { cronJob.Status.LastScheduleTime = nil } cronJob.Status.Active = nil for _, activeJob := range activeJobs { jobRef, err := ref.GetReference(r.Scheme, activeJob) if err != nil { log.Error(err, "unable to make reference to active job", "job", activeJob) continue } cronJob.Status.Active = append(cronJob.Status.Active, *jobRef) } /* Here, we'll log how many jobs we observed at a slightly higher logging level, for debugging. Notice how instead of using a format string, we use a fixed message, and attach key-value pairs with the extra information. This makes it easier to filter and query log lines. */ log.V(1).Info("job count", "active jobs", len(activeJobs), "successful jobs", len(successfulJobs), "failed jobs", len(failedJobs)) // Check if CronJob is suspended isSuspended := cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend // Update status conditions based on current state if isSuspended { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionFalse, Reason: "Suspended", Message: "CronJob is suspended", }) } else if len(failedJobs) > 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "JobsFailed", Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)), }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionFalse, Reason: "JobsFailed", Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)), }) } else if len(activeJobs) > 0 { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionTrue, Reason: "JobsActive", Message: fmt.Sprintf("%d job(s) are currently active", len(activeJobs)), }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionTrue, Reason: "JobsActive", Message: fmt.Sprintf("CronJob is progressing with %d active job(s)", len(activeJobs)), }) } else { meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeAvailableCronJob, Status: metav1.ConditionTrue, Reason: "AllJobsCompleted", Message: "All jobs have completed successfully", }) meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionFalse, Reason: "NoJobsActive", Message: "No jobs are currently active", }) } /* Using the data we've gathered, we'll update the status of our CRD. Just like before, we use our client. To specifically update the status subresource, we'll use the` + " `" + `Status` + "`" + ` part of the client, with the` + " `" + `Update` + "`" + ` method. The status subresource ignores changes to spec, so it's less likely to conflict with any other updates, and can have separate permissions. */ if err := r.Status().Update(ctx, &cronJob); err != nil { log.Error(err, "unable to update CronJob status") return ctrl.Result{}, err } /* Once we've updated our status, we can move on to ensuring that the status of the world matches what we want in our spec. ### 3: Clean up old jobs according to the history limit First, we'll try to clean up old jobs, so that we don't leave too many lying around. */ // NB: deleting these are "best effort" -- if we fail on a particular one, // we won't requeue just to finish the deleting. if cronJob.Spec.FailedJobsHistoryLimit != nil { slices.SortStableFunc(failedJobs, func(a, b *kbatch.Job) int { aStartTime := a.Status.StartTime bStartTime := b.Status.StartTime if aStartTime == nil && bStartTime != nil { return 1 } if aStartTime.Before(bStartTime) { return -1 } else if bStartTime.Before(aStartTime) { return 1 } return 0 }) for i, job := range failedJobs { if int32(i) >= int32(len(failedJobs))-*cronJob.Spec.FailedJobsHistoryLimit { break } if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { log.Error(err, "unable to delete old failed job", "job", job) } else { log.V(1).Info("deleted old failed job", "job", job) } } } if cronJob.Spec.SuccessfulJobsHistoryLimit != nil { slices.SortStableFunc(successfulJobs, func(a, b *kbatch.Job) int { aStartTime := a.Status.StartTime bStartTime := b.Status.StartTime if aStartTime == nil && bStartTime != nil { return 1 } if aStartTime.Before(bStartTime) { return -1 } else if bStartTime.Before(aStartTime) { return 1 } return 0 }) for i, job := range successfulJobs { if int32(i) >= int32(len(successfulJobs))-*cronJob.Spec.SuccessfulJobsHistoryLimit { break } if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil { log.Error(err, "unable to delete old successful job", "job", job) } else { log.V(1).Info("deleted old successful job", "job", job) } } } /* ### 4: Check if we're suspended If this object is suspended, we don't want to run any jobs, so we'll stop now. This is useful if something's broken with the job we're running and we want to pause runs to investigate or putz with the cluster, without deleting the object. */ if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend { log.V(1).Info("cronjob suspended, skipping") return ctrl.Result{}, nil } /* ### 5: Get the next scheduled run If we're not paused, we'll need to calculate the next scheduled run, and whether or not we've got a run that we haven't processed yet. */ /* We'll calculate the next scheduled time using our helpful cron library. We'll start calculating appropriate times from our last run, or the creation of the CronJob if we can't find a last run. If there are too many missed runs and we don't have any deadlines set, we'll bail so that we don't cause issues on controller restarts or wedges. Otherwise, we'll just return the missed runs (of which we'll just use the latest), and the next run, so that we can know when it's time to reconcile again. */ getNextSchedule := func(cronJob *batchv1.CronJob, now time.Time) (lastMissed time.Time, next time.Time, err error) { sched, err := cron.ParseStandard(cronJob.Spec.Schedule) if err != nil { return time.Time{}, time.Time{}, fmt.Errorf("unparseable schedule %q: %w", cronJob.Spec.Schedule, err) } // for optimization purposes, cheat a bit and start from our last observed run time // we could reconstitute this here, but there's not much point, since we've // just updated it. var earliestTime time.Time if cronJob.Status.LastScheduleTime != nil { earliestTime = cronJob.Status.LastScheduleTime.Time } else { earliestTime = cronJob.CreationTimestamp.Time } if cronJob.Spec.StartingDeadlineSeconds != nil { // controller is not going to schedule anything below this point schedulingDeadline := now.Add(-time.Second * time.Duration(*cronJob.Spec.StartingDeadlineSeconds)) if schedulingDeadline.After(earliestTime) { earliestTime = schedulingDeadline } } if earliestTime.After(now) { return time.Time{}, sched.Next(now), nil } starts := 0 for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) { lastMissed = t // An object might miss several starts. For example, if // controller gets wedged on Friday at 5:01pm when everyone has // gone home, and someone comes in on Tuesday AM and discovers // the problem and restarts the controller, then all the hourly // jobs, more than 80 of them for one hourly scheduledJob, should // all start running with no further intervention (if the scheduledJob // allows concurrency and late starts). // // However, if there is a bug somewhere, or incorrect clock // on controller's server or apiservers (for setting creationTimestamp) // then there could be so many missed start times (it could be off // by decades or more), that it would eat up all the CPU and memory // of this controller. In that case, we want to not try to list // all the missed start times. starts++ if starts > 100 { // We can't get the most recent times so just return an empty slice return time.Time{}, time.Time{}, fmt.Errorf("Too many missed start times (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.") //nolint:staticcheck } } return lastMissed, sched.Next(now), nil } // +kubebuilder:docs-gen:collapse=getNextSchedule // figure out the next times that we need to create // jobs at (or anything we missed). missedRun, nextRun, err := getNextSchedule(&cronJob, r.Now()) if err != nil { log.Error(err, "unable to figure out CronJob schedule") if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the schedule error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "InvalidSchedule", Message: fmt.Sprintf("Failed to parse schedule: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } // we don't really care about requeuing until we get an update that // fixes the schedule, so don't return an error return ctrl.Result{}, nil } /* We'll prep our eventual request to requeue until the next job, and then figure out if we actually need to run. */ scheduledResult := ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())} // save this so we can re-use it elsewhere log = log.WithValues("now", r.Now(), "next run", nextRun) /* ### 6: Run a new job if it's on schedule, not past the deadline, and not blocked by our concurrency policy If we've missed a run, and we're still within the deadline to start it, we'll need to run a job. */ if missedRun.IsZero() { log.V(1).Info("no upcoming scheduled times, sleeping until next") return scheduledResult, nil } // make sure we're not too late to start the run log = log.WithValues("current run", missedRun) tooLate := false if cronJob.Spec.StartingDeadlineSeconds != nil { tooLate = missedRun.Add(time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second).Before(r.Now()) } if tooLate { log.V(1).Info("missed starting deadline for last run, sleeping till next") if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect missed deadline meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "MissedSchedule", Message: fmt.Sprintf("Missed starting deadline for run at %v", missedRun), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return scheduledResult, nil } /* If we actually have to run a job, we'll need to either wait till existing ones finish, replace the existing ones, or just add new ones. If our information is out of date due to cache delay, we'll get a requeue when we get up-to-date information. */ // figure out how to run this job -- concurrency policy might forbid us from running // multiple at the same time... if cronJob.Spec.ConcurrencyPolicy == batchv1.ForbidConcurrent && len(activeJobs) > 0 { log.V(1).Info("concurrency policy blocks concurrent runs, skipping", "num active", len(activeJobs)) return scheduledResult, nil } // ...or instruct us to replace existing ones... if cronJob.Spec.ConcurrencyPolicy == batchv1.ReplaceConcurrent { for _, activeJob := range activeJobs { // we don't care if the job was already deleted if err := r.Delete(ctx, activeJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil { log.Error(err, "unable to delete active job", "job", activeJob) return ctrl.Result{}, err } } } /* Once we've figured out what to do with existing jobs, we'll actually create our desired job */ /* We need to construct a job based on our CronJob's template. We'll copy over the spec from the template and copy some basic object meta. Then, we'll set the "scheduled time" annotation so that we can reconstitute our ` + "`" + `LastScheduleTime` + "`" + ` field each reconcile. Finally, we'll need to set an owner reference. This allows the Kubernetes garbage collector to clean up jobs when we delete the CronJob, and allows controller-runtime to figure out which cronjob needs to be reconciled when a given job changes (is added, deleted, completes, etc). */ constructJobForCronJob := func(cronJob *batchv1.CronJob, scheduledTime time.Time) (*kbatch.Job, error) { // We want job names for a given nominal start time to have a deterministic name to avoid the same job being created twice name := fmt.Sprintf("%s-%d", cronJob.Name, scheduledTime.Unix()) job := &kbatch.Job{ ObjectMeta: metav1.ObjectMeta{ Labels: make(map[string]string), Annotations: make(map[string]string), Name: name, Namespace: cronJob.Namespace, }, Spec: *cronJob.Spec.JobTemplate.Spec.DeepCopy(), } maps.Copy(job.Annotations, cronJob.Spec.JobTemplate.Annotations) job.Annotations[scheduledTimeAnnotation] = scheduledTime.Format(time.RFC3339) maps.Copy(job.Labels, cronJob.Spec.JobTemplate.Labels) if err := ctrl.SetControllerReference(cronJob, job, r.Scheme); err != nil { return nil, err } return job, nil } // +kubebuilder:docs-gen:collapse=constructJobForCronJob // actually make the job... job, err := constructJobForCronJob(&cronJob, missedRun) if err != nil { log.Error(err, "unable to construct job from template") // don't bother requeuing until we get a change to the spec return scheduledResult, nil } // ...and create it on the cluster if err := r.Create(ctx, job); err != nil { log.Error(err, "unable to create Job for CronJob", "job", job) if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect the error meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeDegradedCronJob, Status: metav1.ConditionTrue, Reason: "JobCreationFailed", Message: fmt.Sprintf("Failed to create job: %v", err), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } return ctrl.Result{}, err } log.V(1).Info("created Job for CronJob run", "job", job) if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil { log.Error(fetchErr, "Failed to re-fetch CronJob") return ctrl.Result{}, fetchErr } // Update status condition to reflect successful job creation meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{ Type: typeProgressingCronJob, Status: metav1.ConditionTrue, Reason: "JobCreated", Message: fmt.Sprintf("Created job %s", job.Name), }) if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil { log.Error(statusErr, "Failed to update CronJob status") } /* ### 7: Requeue when we either see a running job or it's time for the next scheduled run Finally, we'll return the result that we prepped above, that says we want to requeue when our next run would need to occur. This is taken as a maximum deadline -- if something else changes in between, like our job starts or finishes, we get modified, etc, we might reconcile again sooner. */ // we'll requeue once we see the running job, and update our status return scheduledResult, nil } /* ### Setup Finally, we'll update our setup. In order to allow our reconciler to quickly look up Jobs by their owner, we'll need an index. We declare an index key that we can later use with the client as a pseudo-field name, and then describe how to extract the indexed value from the Job object. The indexer will automatically take care of namespaces for us, so we just have to extract the owner name if the Job has a CronJob owner. Additionally, we'll inform the manager that this controller owns some Jobs, so that it will automatically call Reconcile on the underlying CronJob when a Job changes, is deleted, etc. */ var ( jobOwnerKey = ".metadata.controller" apiGVStr = batchv1.GroupVersion.String() ) ` const controllerSetupWithManager = ` // set up a real clock, since we're not in a test if r.Clock == nil { r.Clock = realClock{} } if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kbatch.Job{}, jobOwnerKey, func(rawObj client.Object) []string { // grab the job object, extract the owner... job := rawObj.(*kbatch.Job) owner := metav1.GetControllerOf(job) if owner == nil { return nil } // ...make sure it's a CronJob... if owner.APIVersion != apiGVStr || owner.Kind != "CronJob" { return nil } // ...and if so, return it return []string{owner.Name} }); err != nil { return err } ` ================================================ FILE: hack/docs/internal/cronjob-tutorial/e2e_implementation.go ================================================ /* Copyright 2025 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 cronjob const isPrometheusInstalledVar = ` // shouldCleanupPrometheus tracks whether Prometheus was installed by this suite. shouldCleanupPrometheus = false` const beforeSuitePrometheus = ` By("Ensure that Prometheus is enabled") _ = utils.UncommentCode("config/default/kustomization.yaml", "#- ../prometheus", "#") ` const afterSuitePrometheus = ` // Teardown Prometheus if it was installed by this suite if shouldCleanupPrometheus { By("uninstalling Prometheus Operator") utils.UninstallPrometheusOperator() } ` const checkPrometheusInstalled = ` By("checking if Prometheus is already installed") if !utils.IsPrometheusCRDsInstalled() { // Mark for cleanup before installation to handle interruptions and partial installs. shouldCleanupPrometheus = true By("installing Prometheus Operator") Expect(utils.InstallPrometheusOperator()).To(Succeed(), "Failed to install Prometheus Operator") } ` const serviceMonitorE2e = ` By("validating that the ServiceMonitor for Prometheus is applied in the namespace") cmd = exec.Command("kubectl", "get", "ServiceMonitor", "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "ServiceMonitor should exist")` const prometheusUtilities = `// InstallPrometheusOperator installs the prometheus Operator to be used to export the enabled metrics. func InstallPrometheusOperator() error { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "create", "-f", url) _, err := Run(cmd) return err } // UninstallPrometheusOperator uninstalls the prometheus func UninstallPrometheusOperator() { url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion) cmd := exec.Command("kubectl", "delete", "-f", url) if _, err := Run(cmd); err != nil { warnError(err) } } // IsPrometheusCRDsInstalled checks if any Prometheus CRDs are installed // by verifying the existence of key CRDs related to Prometheus. func IsPrometheusCRDsInstalled() bool { // List of common Prometheus CRDs prometheusCRDs := []string{ "prometheuses.monitoring.coreos.com", "prometheusrules.monitoring.coreos.com", "prometheusagents.monitoring.coreos.com", } cmd := exec.Command("kubectl", "get", "crds", "-o", "custom-columns=NAME:.metadata.name") output, err := Run(cmd) if err != nil { return false } crdList := GetNonEmptyLines(output) for _, crd := range prometheusCRDs { for _, line := range crdList { if strings.Contains(line, crd) { return true } } } return false } ` const prometheusVersionURL = ` prometheusOperatorVersion = "v0.89.0" prometheusOperatorURL = "https://github.com/prometheus-operator/prometheus-operator/" + "releases/download/%s/bundle.yaml"` ================================================ FILE: hack/docs/internal/cronjob-tutorial/generate_cronjob.go ================================================ /* Copyright 2023 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 cronjob import ( "fmt" log "log/slog" "os/exec" "path/filepath" "github.com/spf13/afero" hackutils "sigs.k8s.io/kubebuilder/v4/hack/docs/internal/utils" pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" ) // Sample define the sample which will be scaffolded type Sample struct { ctx *utils.TestContext } // NewSample create a new instance of the cronjob sample and configure the KB CLI that will be used func NewSample(binaryPath, samplePath string) Sample { log.Info("Generating the sample context of Cronjob...") ctx := hackutils.NewSampleContext(binaryPath, samplePath, "GO111MODULE=on") return Sample{&ctx} } // Prepare the Context for the sample project func (sp *Sample) Prepare() { log.Info("destroying directory for cronjob sample project") sp.ctx.Destroy() log.Info("refreshing tools and creating directory...") err := sp.ctx.Prepare() hackutils.CheckError("creating directory for sample project", err) } // GenerateSampleProject will generate the sample func (sp *Sample) GenerateSampleProject() { log.Info("Initializing the cronjob project") err := sp.ctx.Init( "--domain", "tutorial.kubebuilder.io", "--repo", "tutorial.kubebuilder.io/project", "--license", "apache2", "--owner", "The Kubernetes authors", ) hackutils.CheckError("Initializing the cronjob project", err) log.Info("Adding a new config type") err = sp.ctx.CreateAPI( "--group", "batch", "--version", "v1", "--kind", "CronJob", "--resource", "--controller", ) hackutils.CheckError("Creating the API", err) log.Info("Implementing admission webhook") err = sp.ctx.CreateWebhook( "--group", "batch", "--version", "v1", "--kind", "CronJob", "--defaulting", "--programmatic-validation", ) hackutils.CheckError("Implementing admission webhook", err) } // UpdateTutorial the cronjob tutorial with the scaffold changes func (sp *Sample) UpdateTutorial() { log.Info("Update tutorial with cronjob code") // 1. update specs sp.updateSpec() // 2. update webhook sp.updateWebhook() // 3. update webhookTests sp.updateWebhookTests() // 4. update makefile sp.updateMakefile() // 5. generate extra files cmd := exec.Command("go", "mod", "tidy") _, err := sp.ctx.Run(cmd) hackutils.CheckError("Failed to run go mod tidy for cronjob tutorial", err) cmd = exec.Command("go", "get", "github.com/robfig/cron") _, err = sp.ctx.Run(cmd) hackutils.CheckError("Failed to get package robfig/cron", err) cmd = exec.Command("make", "generate", "manifests") _, err = sp.ctx.Run(cmd) hackutils.CheckError("run make generate and manifests", err) // 6. compensate other intro in API sp.updateAPIStuff() // 7. update reconciliation and main.go // 7.1 update controller sp.updateController() // 7.2 update main.go sp.updateMain() // 8. generate extra files cmd = exec.Command("make", "generate", "manifests") _, err = sp.ctx.Run(cmd) hackutils.CheckError("run make generate and manifests", err) // 9. update suite_test explanation sp.updateSuiteTest() // 10. uncomment kustomization sp.updateKustomization() // 11. add example sp.updateExample() // 12. add test sp.addControllerTest() // 13. update e2e tests sp.updateE2E() } // CodeGen is a noop for this sample, just to make generation of all samples // more efficient. We may want to refactor `UpdateTutorial` some day to take // advantage of a separate call, but it is not necessary. func (sp *Sample) CodeGen() { cmd := exec.Command("make", "all") _, err := sp.ctx.Run(cmd) hackutils.CheckError("Failed to run make all for cronjob tutorial", err) cmd = exec.Command("make", "build-installer") _, err = sp.ctx.Run(cmd) hackutils.CheckError("Failed to run make build-installer for cronjob tutorial", err) err = sp.ctx.EditHelmPlugin() hackutils.CheckError("Failed to enable helm plugin", err) } // insert code to fix docs func (sp *Sample) updateSpec() { var err error err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `limitations under the License. */`, ` // +kubebuilder:docs-gen:collapse=Apache License /* */`) hackutils.CheckError("fixing collapse for cronjob_types.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `package v1`, ` /* */`) hackutils.CheckError("fixing package for cronjob_types.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `import (`, ` batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1"`) hackutils.CheckError("fixing imports for cronjob_types.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `to be serialized.`, cronjobSpecExplaination) hackutils.CheckError("fixing spec explanation for cronjob_types.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `type CronJobSpec struct {`, cronjobSpecStruct) hackutils.CheckError("fixing spec struct for cronjob_types.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // The following markers will use OpenAPI v3 schema to validate the value // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html // foo is an example field of CronJob. Edit cronjob_types.go to remove/update // +optional Foo *string`+" `"+`json:"foo,omitempty"`+"`", "") hackutils.CheckError("fixing additional spec fields for cronjob_types.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `// Important: Run "make" to regenerate code after modifying this file`, cronjobList) hackutils.CheckError("fixing cronjob_types.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `// +kubebuilder:object:root=true // +kubebuilder:subresource:status`, docCommentStatusSub) hackutils.CheckError("fixing cronjob_types.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `SchemeBuilder.Register(&CronJob{}, &CronJobList{}) }`, ` // +kubebuilder:docs-gen:collapse=Root Object Definitions`) hackutils.CheckError("fixing builder for cronjob_types.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `// CronJob is the Schema for the cronjobs API type CronJob struct {`, `// CronJob is the Schema for the cronjobs API type CronJob struct {`+` /* */`) hackutils.CheckError("fixing schema for cronjob_types.go", err) // fix lint err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `/ }`, "/") hackutils.CheckError("fixing cronjob_types.go end of status", err) } func (sp *Sample) updateAPIStuff() { var err error // fix groupversion_info err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/groupversion_info.go"), `limitations under the License. */`, groupVersionIntro) hackutils.CheckError("fixing groupversion_info.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "api/v1/groupversion_info.go"), ` "sigs.k8s.io/controller-runtime/pkg/scheme" )`, groupVersionSchema) hackutils.CheckError("fixing groupversion_info.go", err) } func (sp *Sample) updateController() { var err error err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), `limitations under the License. */`, controllerIntro) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), `import ( "context" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" batchv1 "tutorial.kubebuilder.io/project/api/v1" )`, controllerImport) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), `Scheme *runtime.Scheme`, ` Clock`) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), ` Clock }`, controllerMockClock) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), `// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update`, controllerReconcile) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), fmt.Sprintf(`// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@%s/pkg/reconcile`, scaffolds.ControllerRuntimeVersion), skipGoCycloLint) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), ` _ = logf.FromContext(ctx) // TODO(user): your logic here return ctrl.Result{}, nil }`, controllerReconcileLogic) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), `SetupWithManager(mgr ctrl.Manager) error {`, controllerSetupWithManager) hackutils.CheckError("fixing cronjob_controller.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller.go"), `For(&batchv1.CronJob{}).`, ` Owns(&kbatch.Job{}).`) hackutils.CheckError("fixing cronjob_controller.go", err) } func (sp *Sample) updateMain() { var err error err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "cmd/main.go"), `limitations under the License. */`, ` // +kubebuilder:docs-gen:collapse=Apache License`) hackutils.CheckError("fixing main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "cmd/main.go"), `// +kubebuilder:scaffold:imports )`, mainBatch) hackutils.CheckError("fixing main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "cmd/main.go"), `// +kubebuilder:scaffold:scheme }`, ` /* The other thing that's changed is that kubebuilder has added a block calling our CronJob controller's`+" `"+`SetupWithManager`+"`"+` method. */`) hackutils.CheckError("fixing main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "cmd/main.go"), `func main() {`, ` /* */`) hackutils.CheckError("fixing main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "cmd/main.go"), `if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) }`, ` // +kubebuilder:docs-gen:collapse=Remaining code from main.go`) hackutils.CheckError("fixing main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "cmd/main.go"), `setupLog.Error(err, "Failed to create controller", "controller", "CronJob") os.Exit(1) }`, mainEnableWebhook) hackutils.CheckError("fixing main.go", err) } func (sp *Sample) updateMakefile() { const originalManifestTarget = `.PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases ` const changedManifestTarget = `.PHONY: manifests manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. # Note that the option maxDescLen=0 was added in the default scaffold in order to sort out the issue # Too long: must have at most 262144 bytes. By using kubectl apply to create / update resources an annotation # is created by K8s API to store the latest version of the resource ( kubectl.kubernetes.io/last-applied-configuration). # However, it has a size limit and if the CRD is too big with so many long descriptions as this one it will cause the failure. "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases ` err := pluginutil.ReplaceInFile(filepath.Join(sp.ctx.Dir, "Makefile"), originalManifestTarget, changedManifestTarget) hackutils.CheckError("updating makefile to use maxDescLen=0 in make manifest target", err) } func (sp *Sample) updateWebhookTests() { file := filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook_test.go") err := pluginutil.InsertCode(file, `// TODO (user): Add any additional imports if needed`, ` "k8s.io/utils/ptr"`) hackutils.CheckError("add import for webhook tests", err) err = pluginutil.ReplaceInFile(file, webhookTestCreateDefaultingFragment, webhookTestCreateDefaultingReplaceFragment) hackutils.CheckError("replace create defaulting test", err) err = pluginutil.ReplaceInFile(file, webhookTestingValidatingTodoFragment, webhookTestingValidatingExampleFragment) hackutils.CheckError("replace validating defaulting test", err) err = pluginutil.ReplaceInFile(file, webhookTestsVars, webhookTestsConstants) hackutils.CheckError("replace before each webhook test ", err) err = pluginutil.ReplaceInFile(file, webhookTestsBeforeEachOriginal, webhookTestsBeforeEachChanged) hackutils.CheckError("replace before each webhook test ", err) } func (sp *Sample) updateWebhook() { var err error err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `limitations under the License. */`, ` // +kubebuilder:docs-gen:collapse=Apache License`) hackutils.CheckError("fixing cronjob_webhook.go by adding collapse", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `import ( "context"`, ` "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field"`, ) hackutils.CheckError("add extra imports to cronjob_webhook.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // nolint:unused // log is for logging in this package. `, webhookIntro) hackutils.CheckError("fixing cronjob_webhook.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `var cronjoblog = logf.Log.WithName("cronjob-resource")`, ` /* Then, we set up the webhook with the manager. */`) hackutils.CheckError("fixing cronjob_webhook.go by setting webhook with manager comment", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!`, webhooksNoticeMarker) hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute for webhook notice ", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.`, explanationValidateCRD) hackutils.CheckError("fixing cronjob_webhook.go by replacing note about path attribute for explanation validate CRD", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.`, "") hackutils.CheckError("fixing cronjob_webhook.go by replace TODO to change verbs", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): Add more fields as needed for defaulting`, fragmentForDefaultFields) hackutils.CheckError("fixing cronjob_webhook.go by replacing TODO in Defaulter", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `WithDefaulter(&CronJobCustomDefaulter{}).`, `WithDefaulter(&CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, }).`) hackutils.CheckError("replacing WithDefaulter call in cronjob_webhook.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): fill in your defaulting logic. return nil }`, webhookDefaultingSettings) hackutils.CheckError("fixing cronjob_webhook.go by adding logic", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): fill in your validation logic upon object creation. return nil, nil`, `return nil, validateCronJob(obj)`) hackutils.CheckError("fixing cronjob_webhook.go by fill in your validation", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// TODO(user): fill in your validation logic upon object update. return nil, nil`, `return nil, validateCronJob(newObj)`) hackutils.CheckError("fixing cronjob_webhook.go by adding validation logic upon object update", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob.`, customInterfaceDefaultInfo) hackutils.CheckError("fixing cronjob_webhook.go by adding validation logic upon object update", err) err = pluginutil.AppendCodeAtTheEnd( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), webhookValidateSpecMethods) hackutils.CheckError("adding validation spec methods at the end", err) } func (sp *Sample) updateSuiteTest() { var err error err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), `limitations under the License. */`, suiteTestIntro) hackutils.CheckError("updating suite_test.go to add license intro", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), ` "time" `, ` ctrl "sigs.k8s.io/controller-runtime" `) hackutils.CheckError("updating suite_test.go to add ctrl import", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), ` var ( ctx context.Context cancel context.CancelFunc testEnv *envtest.Environment cfg *rest.Config k8sClient client.Client ) `, suiteTestEnv) hackutils.CheckError("updating suite_test.go to add more variables", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), ` err = batchv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) // +kubebuilder:scaffold:scheme `, suiteTestAddSchema) hackutils.CheckError("updating suite_test.go to add schema", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), `testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir() }`, ` /* Then, we start the envtest cluster. */`) hackutils.CheckError("updating suite_test.go to add text to show where envtest cluster start", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), ` cfg, err = testEnv.Start() Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) `, suiteTestClient) hackutils.CheckError("updating suite_test.go to add text about test client", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), ` k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) `, suiteTestDescription) hackutils.CheckError("updating suite_test.go for test description", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "internal/controller/suite_test.go"), ` var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) `, suiteTestCleanup) hackutils.CheckError("updating suite_test.go to cleanup tests", err) } func (sp *Sample) updateKustomization() { var err error err = pluginutil.UncommentCode( filepath.Join(sp.ctx.Dir, "config/default/kustomization.yaml"), `#- ../prometheus`, `#`) hackutils.CheckError("fixing default/kustomization", err) err = pluginutil.UncommentCode( filepath.Join(sp.ctx.Dir, "config/default/kustomization.yaml"), `#- path: cert_metrics_manager_patch.yaml # target: # kind: Deployment`, `#`) hackutils.CheckError("enabling cert_metrics_manager_patch.yaml", err) err = pluginutil.UncommentCode( filepath.Join(sp.ctx.Dir, "config/prometheus/kustomization.yaml"), `#patches: # - path: monitor_tls_patch.yaml # target: # kind: ServiceMonitor`, `#`) hackutils.CheckError("enabling monitor tls patch", err) err = pluginutil.UncommentCode( filepath.Join(sp.ctx.Dir, "config/default/kustomization.yaml"), certManagerForMetrics, `#`) hackutils.CheckError("fixing default/kustomization", err) // Add ANCHOR markers for webhook documentation sections // This allows the docs to include only webhook-related parts of the kustomization.yaml // Add anchor for webhook resources section err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "config/default/kustomization.yaml"), `# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager`, `# ANCHOR: webhook-resources # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. - ../certmanager # ANCHOR_END: webhook-resources`) hackutils.CheckError("adding webhook-resources anchors", err) // Add anchor for webhook patch section err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "config/default/kustomization.yaml"), `# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - path: manager_webhook_patch.yaml target: kind: Deployment`, `# ANCHOR: webhook-patch # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml - path: manager_webhook_patch.yaml target: kind: Deployment # ANCHOR_END: webhook-patch`) hackutils.CheckError("adding webhook-patch anchors", err) // Add anchor for webhook replacements section (from webhook service to MutatingWebhookConfiguration) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "config/default/kustomization.yaml"), ` - source: # Uncomment the following block if you have any webhook kind: Service version: v1 name: webhook-service fieldPath: .metadata.name # Name of the service`, ` # ANCHOR: webhook-replacements - source: # Uncomment the following block if you have any webhook kind: Service version: v1 name: webhook-service fieldPath: .metadata.name # Name of the service`) hackutils.CheckError("adding webhook-replacements anchor start", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "config/default/kustomization.yaml"), ` create: true # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)`, ` create: true # ANCHOR_END: webhook-replacements # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)`) hackutils.CheckError("adding webhook-replacements anchor end", err) } func (sp *Sample) updateExample() { var err error // samples/batch_v1_cronjob err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "config/samples/batch_v1_cronjob.yaml"), `spec:`, cronjobSample) hackutils.CheckError("fixing samples/batch_v1_cronjob.yaml", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "config/samples/batch_v1_cronjob.yaml"), `# TODO(user): Add fields here`, "") hackutils.CheckError("fixing samples/batch_v1_cronjob.yaml", err) } func (sp *Sample) addControllerTest() { fs := afero.NewOsFs() err := afero.WriteFile(fs, filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller_test.go"), []byte(controllerTest), 0o600) hackutils.CheckError("adding cronjob_controller_test", err) } func (sp *Sample) updateE2E() { cronjobE2ESuite := filepath.Join(sp.ctx.Dir, "test", "e2e", "e2e_suite_test.go") cronjobE2ETest := filepath.Join(sp.ctx.Dir, "test", "e2e", "e2e_test.go") cronjobE2EUtils := filepath.Join(sp.ctx.Dir, "test", "utils", "utils.go") var err error err = pluginutil.InsertCode(cronjobE2ESuite, `shouldCleanupCertManager = false`, isPrometheusInstalledVar) hackutils.CheckError("fixing test/e2e/e2e_suite_test.go by adding isPrometheusInstalledVar", err) err = pluginutil.InsertCode(cronjobE2ESuite, `var _ = BeforeSuite(func() {`, beforeSuitePrometheus) hackutils.CheckError("fixing test/e2e/e2e_suite_test.go by adding prometheus code in the before suite", err) err = pluginutil.InsertCode(cronjobE2ESuite, `setupCertManager()`, checkPrometheusInstalled) hackutils.CheckError("fixing test/e2e/e2e_suite_test.go by adding code check if has prometheus", err) err = pluginutil.InsertCode(cronjobE2EUtils, `defaultKindCluster = "kind"`, prometheusVersionURL) hackutils.CheckError("fixing test/e2e/e2e_suite_test.go by adding prometheus version and URL", err) err = pluginutil.InsertCode(cronjobE2EUtils, `return false } `, prometheusUtilities) hackutils.CheckError("fixing test/e2e/e2e_suite_test.go by adding prometheus version and URL", err) err = pluginutil.InsertCode(cronjobE2ESuite, `var _ = AfterSuite(func() {`, afterSuitePrometheus) hackutils.CheckError("fixing test/e2e/e2e_suite_test.go by adding prometheus code after suite", err) err = pluginutil.InsertCode(cronjobE2ETest, `Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")`, serviceMonitorE2e) hackutils.CheckError("fixing test/e2e/e2e_test.go by adding ServiceMonitor should exist", err) } ================================================ FILE: hack/docs/internal/cronjob-tutorial/main_revisited.go ================================================ /* Copyright 2023 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 cronjob const mainBatch = ` // +kubebuilder:docs-gen:collapse=Imports /* The first difference to notice is that kubebuilder has added the new API group's package (` + "`" + `batchv1` + "`" + `) to our scheme. This means that we can use those objects in our controller. If we would be using any other CRD we would have to add their scheme the same way. Builtin types such as Job have their scheme added by` + " `" + `clientgoscheme` + "`" + `. */` const mainEnableWebhook = ` /* We'll also set up webhooks for our type, which we'll talk about next. We just need to add them to the manager. Since we might want to run the webhooks separately, or not run them when testing our controller locally, we'll put them behind an environment variable. We'll just make sure to set` + " `" + `ENABLE_WEBHOOKS=false` + "`" + ` when we run locally. */` ================================================ FILE: hack/docs/internal/cronjob-tutorial/other_api_files.go ================================================ /* Copyright 2023 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 cronjob const groupVersionIntro = ` // +kubebuilder:docs-gen:collapse=Apache License /* First, we have some *package-level* markers that denote that there are Kubernetes objects in this package, and that this package represents the group ` + "`" + `batch.tutorial.kubebuilder.io` + "`" + `. The` + " `" + `object` + "`" + ` generator makes use of the former, while the latter is used by the CRD generator to generate the right metadata for the CRDs it creates from this package. */ ` const groupVersionSchema = ` /* Then, we have the commonly useful variables that help us set up our Scheme. Since we need to use all the types in this package in our controller, it's helpful (and the convention) to have a convenient method to add all the types to some other` + " `" + `Scheme` + "`" + `. SchemeBuilder makes this easy for us. */` ================================================ FILE: hack/docs/internal/cronjob-tutorial/sample.go ================================================ /* Copyright 2023 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 cronjob const cronjobSample = ` schedule: "*/1 * * * *" startingDeadlineSeconds: 60 concurrencyPolicy: Allow # explicitly specify, but Allow is also default. jobTemplate: spec: template: spec: securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false restartPolicy: OnFailure` const certManagerForMetrics = `# - source: # Uncomment the following block to enable certificates for metrics # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.name # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 0 # create: true # - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor # kind: ServiceMonitor # group: monitoring.coreos.com # version: v1 # name: controller-manager-metrics-monitor # fieldPaths: # - spec.endpoints.0.tlsConfig.serverName # options: # delimiter: '.' # index: 0 # create: true # - source: # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.namespace # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 1 # create: true # - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor # kind: ServiceMonitor # group: monitoring.coreos.com # version: v1 # name: controller-manager-metrics-monitor # fieldPaths: # - spec.endpoints.0.tlsConfig.serverName # options: # delimiter: '.' # index: 1 # create: true` ================================================ FILE: hack/docs/internal/cronjob-tutorial/webhook_implementation.go ================================================ /* Copyright 2023 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 cronjob const webhookIntro = `batchv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Imports /* Next, we'll setup a logger for the webhooks. */ ` const webhookDefaultingSettings = `// Set default values d.applyDefaults(obj) return nil } // applyDefaults applies default values to CronJob fields. func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) { if cronJob.Spec.ConcurrencyPolicy == "" { cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } if cronJob.Spec.Suspend == nil { cronJob.Spec.Suspend = new(bool) *cronJob.Spec.Suspend = d.DefaultSuspend } if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } if cronJob.Spec.FailedJobsHistoryLimit == nil { cronJob.Spec.FailedJobsHistoryLimit = new(int32) *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } ` const webhooksNoticeMarker = ` /* Notice that we use kubebuilder markers to generate webhook manifests. This marker is responsible for generating a mutating webhook manifest. The meaning of each marker can be found [here](/reference/markers/webhook.md). */ /* This marker is responsible for generating a mutation webhook manifest. */ ` const explanationValidateCRD = ` /* We can validate our CRD beyond what's possible with declarative validation. Generally, declarative validation should be sufficient, but sometimes more advanced use cases call for complex validation. For instance, we'll see below that we use this to validate a well-formed cron schedule without making up a long regular expression. If` + " `" + `webhook.CustomValidator` + "`" + ` interface is implemented, a webhook will automatically be served that calls the validation. The` + " `" + `ValidateCreate` + "`" + `, ` + "`" + `ValidateUpdate` + "`" + ` and` + " `" + `ValidateDelete` + "`" + ` methods are expected to validate its receiver upon creation, update and deletion respectively. We separate out ValidateCreate from ValidateUpdate to allow behavior like making certain fields immutable, so that they can only be set on creation. ValidateDelete is also separated from ValidateUpdate to allow different validation behavior on deletion. Here, however, we just use the same shared validation for` + " `" + `ValidateCreate` + "`" + ` and ` + "`" + `ValidateUpdate` + "`" + `. And we do nothing in` + " `" + `ValidateDelete` + "`" + `, since we don't need to validate anything on deletion. */ /* This marker is responsible for generating a validation webhook manifest. */ // TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.` const customInterfaceDefaultInfo = `/* We use the ` + "`" + `webhook.CustomDefaulter` + "`" + `interface to set defaults to our CRD. A webhook will automatically be served that calls this defaulting. The ` + "`" + `Default` + "`" + `method is expected to mutate the receiver, setting the defaults. */ // Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob.` const webhookValidateSpecMethods = ` /* We validate the name and the spec of the CronJob. */ // validateCronJob validates the fields of a CronJob object. func validateCronJob(cronjob *batchv1.CronJob) error { var allErrs field.ErrorList if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil } return apierrors.NewInvalid( schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs) } /* Some fields are declaratively validated by OpenAPI schema. You can find kubebuilder validation markers (prefixed with ` + "`" + `// +kubebuilder:validation` + "`" + `) in the [Designing an API](api-design.md) section. You can find all of the kubebuilder supported markers for declaring validation by running ` + "`" + `controller-gen crd -w` + "`" + `, or [here](/reference/markers/crd-validation.md). */ func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error { // The field helpers from the kubernetes API machinery help us return nicely // structured validation errors. return validateScheduleFormat( cronjob.Spec.Schedule, field.NewPath("spec").Child("schedule")) } /* We'll need to validate the [cron](https://en.wikipedia.org/wiki/Cron) schedule is well-formatted. */ func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error { if _, err := cron.ParseStandard(schedule); err != nil { return field.Invalid(fldPath, schedule, err.Error()) } return nil } /* Validating the length of a string field can be done declaratively by the validation schema. But the ` + "`" + `ObjectMeta.Name` + "`" + ` field is defined in a shared package under the apimachinery repo, so we can't declaratively validate it using the validation schema. */ func validateCronJobName(cronjob *batchv1.CronJob) *field.Error { if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 { // The job name length is 63 characters like all Kubernetes objects // (which must fit in a DNS subdomain). The cronjob controller appends // a 11-character suffix to the cronjob (` + "`" + `-$TIMESTAMP` + "`" + `) when creating // a job. The job name length limit is 63 characters. Therefore cronjob // names must have length <= 63-11=52. If we don't validate this here, // then job creation will fail later. return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters") } return nil } // +kubebuilder:docs-gen:collapse=validateCronJobName() Code Implementation` const fragmentForDefaultFields = ` // Default values for various CronJob fields DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 ` const webhookTestCreateDefaultingFragment = `// TODO (user): Add logic for defaulting webhooks // Example: // It("Should apply defaults when a required field is empty", func() { // By("simulating a scenario where defaults should be applied") // obj.SomeFieldWithDefault = "" // By("calling the Default method to apply defaults") // defaulter.Default(ctx, obj) // By("checking that the default values are set") // Expect(obj.SomeFieldWithDefault).To(Equal("default_value")) // })` const webhookTestCreateDefaultingReplaceFragment = `It("Should apply defaults when a required field is empty", func() { By("simulating a scenario where defaults should be applied") obj.Spec.ConcurrencyPolicy = "" // This should default to AllowConcurrent obj.Spec.Suspend = nil // This should default to false obj.Spec.SuccessfulJobsHistoryLimit = nil // This should default to 3 obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1 By("calling the Default method to apply defaults") _ = defaulter.Default(ctx, obj) By("checking that the default values are set") Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent") Expect(*obj.Spec.Suspend).To(BeFalse(), "Expected Suspend to default to false") Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(3)), "Expected SuccessfulJobsHistoryLimit to default to 3") Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(1)), "Expected FailedJobsHistoryLimit to default to 1") }) It("Should not overwrite fields that are already set", func() { By("setting fields that would normally get a default") obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent obj.Spec.Suspend = ptr.To(true) obj.Spec.SuccessfulJobsHistoryLimit = ptr.To(int32(5)) obj.Spec.FailedJobsHistoryLimit = ptr.To(int32(2)) By("calling the Default method to apply defaults") _ = defaulter.Default(ctx, obj) By("checking that the fields were not overwritten") Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value") Expect(obj.Spec.Suspend).NotTo(BeNil()) Expect(*obj.Spec.Suspend).To(BeTrue(), "Expected Suspend to retain its set value") Expect(obj.Spec.SuccessfulJobsHistoryLimit).NotTo(BeNil()) Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(5)), "Expected SuccessfulJobsHistoryLimit to retain its set value") Expect(obj.Spec.FailedJobsHistoryLimit).NotTo(BeNil()) Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(2)), "Expected FailedJobsHistoryLimit to retain its set value") })` const webhookTestingValidatingTodoFragment = `// TODO (user): Add logic for validating webhooks // Example: // It("Should deny creation if a required field is missing", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "" // Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred()) // }) // // It("Should admit creation if all required fields are present", func() { // By("simulating an invalid creation scenario") // obj.SomeRequiredField = "valid_value" // Expect(validator.ValidateCreate(ctx, obj)).To(BeNil()) // }) // // It("Should validate updates correctly", func() { // By("simulating a valid update scenario") // oldObj.SomeRequiredField = "updated_value" // obj.SomeRequiredField = "updated_value" // Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil()) // })` const webhookTestingValidatingExampleFragment = `It("Should deny creation if the name is too long", func() { obj.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" Expect(validator.ValidateCreate(ctx, obj)).Error().To( MatchError(ContainSubstring("must be no more than 52 characters")), "Expected name validation to fail for a too-long name") }) It("Should admit creation if the name is valid", func() { obj.Name = validCronJobName Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), "Expected name validation to pass for a valid name") }) It("Should deny creation if the schedule is invalid", func() { obj.Spec.Schedule = "invalid-cron-schedule" Expect(validator.ValidateCreate(ctx, obj)).Error().To( MatchError(ContainSubstring("Expected exactly 5 fields, found 1: invalid-cron-schedule")), "Expected spec validation to fail for an invalid schedule") }) It("Should admit creation if the schedule is valid", func() { obj.Spec.Schedule = schedule Expect(validator.ValidateCreate(ctx, obj)).To(BeNil(), "Expected spec validation to pass for a valid schedule") }) It("Should deny update if both name and spec are invalid", func() { oldObj.Name = validCronJobName oldObj.Spec.Schedule = schedule By("simulating an update") obj.Name = "this-name-is-way-too-long-and-should-fail-validation-because-it-is-way-too-long" obj.Spec.Schedule = "invalid-cron-schedule" By("validating an update") Expect(validator.ValidateUpdate(ctx, oldObj, obj)).Error().To(HaveOccurred(), "Expected validation to fail for both name and spec") }) It("Should admit update if both name and spec are valid", func() { oldObj.Name = validCronJobName oldObj.Spec.Schedule = schedule By("simulating an update") obj.Name = "valid-cronjob-name-updated" obj.Spec.Schedule = "0 0 * * *" By("validating an update") Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil(), "Expected validation to pass for a valid update") })` const webhookTestsVars = `var ( obj *batchv1.CronJob oldObj *batchv1.CronJob validator CronJobCustomValidator defaulter CronJobCustomDefaulter )` const webhookTestsConstants = ` var ( obj *batchv1.CronJob oldObj *batchv1.CronJob validator CronJobCustomValidator defaulter CronJobCustomDefaulter ) const validCronJobName = "valid-cronjob-name" const schedule = "*/5 * * * *"` const webhookTestsBeforeEachOriginal = `obj = &batchv1.CronJob{} oldObj = &batchv1.CronJob{} validator = CronJobCustomValidator{} Expect(validator).NotTo(BeNil(), "Expected validator to be initialized") defaulter = CronJobCustomDefaulter{} Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized") Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")` const webhookTestsBeforeEachChanged = `obj = &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: schedule, ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: ptr.To(int32(3)), FailedJobsHistoryLimit: ptr.To(int32(1)), }, } *obj.Spec.SuccessfulJobsHistoryLimit = 3 *obj.Spec.FailedJobsHistoryLimit = 1 oldObj = &batchv1.CronJob{ Spec: batchv1.CronJobSpec{ Schedule: schedule, ConcurrencyPolicy: batchv1.AllowConcurrent, SuccessfulJobsHistoryLimit: ptr.To(int32(3)), FailedJobsHistoryLimit: ptr.To(int32(1)), }, } *oldObj.Spec.SuccessfulJobsHistoryLimit = 3 *oldObj.Spec.FailedJobsHistoryLimit = 1 validator = CronJobCustomValidator{} defaulter = CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv1.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, } Expect(obj).NotTo(BeNil(), "Expected obj to be initialized") Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")` ================================================ FILE: hack/docs/internal/cronjob-tutorial/writing_tests_controller.go ================================================ /* Copyright 2023 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 cronjob const controllerTest = `/* 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. */ // +kubebuilder:docs-gen:collapse=Apache License /* Ideally, we should have one` + " `" + `_controller_test.go` + "`" + ` for each controller scaffolded and called in the` + " `" + `suite_test.go` + "`" + `. So, let's write our example test for the CronJob controller (` + "`" + `cronjob_controller_test.go.` + "`" + `) */ /* As usual, we start with the necessary imports. */ package controller import ( "context" "reflect" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" cronjobv1 "tutorial.kubebuilder.io/project/api/v1" ) // +kubebuilder:docs-gen:collapse=Imports /* The first step to writing a simple integration test is to actually create an instance of CronJob you can run tests against. Note that to create a CronJob, you'll need to create a stub CronJob struct that contains your CronJob's specifications. Note that when we create a stub CronJob, the CronJob also needs stubs of its required downstream objects. Without the stubbed Job template spec and the Pod template spec below, the Kubernetes API will not be able to create the CronJob. */ var _ = Describe("CronJob controller", func() { Context("CronJob controller test", func() { const CronjobName = "test-cronjob" ctx := context.Background() namespace := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: CronjobName, Namespace: CronjobName, }, } typeNamespacedName := types.NamespacedName{ Name: CronjobName, Namespace: CronjobName, } cronJob := &cronjobv1.CronJob{} SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) BeforeEach(func() { By("Creating the Namespace to perform the tests") err := k8sClient.Get(ctx, types.NamespacedName{Name: CronjobName}, &v1.Namespace{}) if err != nil && errors.IsNotFound(err) { err = k8sClient.Create(ctx, namespace) Expect(err).NotTo(HaveOccurred()) } By("creating the custom resource for the Kind CronJob") cronJob = &cronjobv1.CronJob{} err = k8sClient.Get(ctx, typeNamespacedName, cronJob) if err != nil && errors.IsNotFound(err) { /* Let's mock our custom resource the same way we would apply it from the manifest under config/samples */ cronJob = &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: CronjobName, Namespace: namespace.Name, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } err = k8sClient.Create(ctx, cronJob) Expect(err).NotTo(HaveOccurred()) } }) /* After each test, we clean up the resources created above. */ AfterEach(func() { By("removing the custom resource for the Kind CronJob") found := &cronjobv1.CronJob{} err := k8sClient.Get(ctx, typeNamespacedName, found) Expect(err).NotTo(HaveOccurred()) Eventually(func(g Gomega) { g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed()) }).Should(Succeed()) // TODO(user): Attention if you improve this code by adding other context test you MUST // be aware of the current delete namespace limitations. // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations By("Deleting the Namespace to perform the tests") _ = k8sClient.Delete(ctx, namespace) }) /* Now we can start implementing the test that validates the controller’s reconciliation behavior. */ It("should successfully reconcile a custom resource for CronJob", func() { By("Checking if the custom resource was successfully created") Eventually(func(g Gomega) { found := &cronjobv1.CronJob{} g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed()) }).Should(Succeed()) /* After creating this CronJob, let's verify that the controller properly initializes the status conditions. The controller runs in the background (started in suite_test.go), so it will automatically detect our CronJob and set initial conditions. */ By("Checking that status conditions are initialized") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Conditions).NotTo(BeEmpty()) }).Should(Succeed()) /* Now let's verify the CronJob has no active jobs initially. We use Gomega's` + " `" + `Consistently()` + "`" + ` check here to ensure the status remains stable, confirming the controller isn't creating jobs prematurely. */ By("Checking that the CronJob has zero active Jobs") Consistently(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(BeEmpty()) }).WithTimeout(time.Second * 10).WithPolling(time.Millisecond * 250).Should(Succeed()) /* Next, we actually create a stubbed Job that will belong to our CronJob. We set the Job's status Active count to 2 to simulate the Job running two pods, which means the Job is actively running. We then set the Job's owner reference to point to our test CronJob. This ensures that the test Job belongs to, and is tracked by, our test CronJob. */ By("Creating a new Job owned by the CronJob") testJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: "test-job", Namespace: namespace.Name, }, Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, } // Note that your CronJob’s GroupVersionKind is required to set up this owner reference. kind := reflect.TypeFor[cronjobv1.CronJob]().Name() gvk := cronjobv1.GroupVersion.WithKind(kind) controllerRef := metav1.NewControllerRef(cronJob, gvk) testJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) Expect(k8sClient.Create(ctx, testJob)).To(Succeed()) // Note that you can not manage the status values while creating the resource. // The status field is managed separately to reflect the current state of the resource. // Therefore, it should be updated using a PATCH or PUT operation after the resource has been created. // Additionally, it is recommended to use StatusConditions to manage the status. For further information see: // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status testJob.Status.Active = 2 Expect(k8sClient.Status().Update(ctx, testJob)).To(Succeed()) /* Adding this Job to our test CronJob should trigger our controller's reconciler logic. After that, we can verify whether our controller eventually updates our CronJob's Status field as expected! */ By("Checking that the CronJob has one active Job in status") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(HaveLen(1), "should have exactly one active job") g.Expect(cronJob.Status.Active[0].Name).To(Equal("test-job"), "the active job name should match") }).Should(Succeed()) /* Finally, let's verify that the controller properly set status conditions. Status conditions are a key part of Kubernetes API conventions and allow users and other controllers to understand the resource state. When there are active jobs, the Available condition should be True with reason JobsActive. */ By("Checking the latest Status Condition added to the CronJob instance") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var conditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &conditions)) Expect(conditions).To(HaveLen(1), "should have one Available condition") Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "Available should be True") Expect(conditions[0].Reason).To(Equal("JobsActive"), "reason should be JobsActive") }) }) }) /* After writing all this code, you can run ` + "`" + `make test` + "`" + ` or ` + "`" + `go test ./...` + "`" + ` in your ` + "`" + `controllers/` + "`" + ` directory again to run your new test! */ ` ================================================ FILE: hack/docs/internal/cronjob-tutorial/writing_tests_env.go ================================================ /* Copyright 2023 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 cronjob const suiteTestIntro = ` // +kubebuilder:docs-gen:collapse=Apache License /* When we created the CronJob API with` + " `" + `kubebuilder create api` + "`" + ` in a [previous chapter](/cronjob-tutorial/new-api.md), Kubebuilder already did some test work for you. Kubebuilder scaffolded a` + " `" + `internal/controller/suite_test.go` + "`" + ` file that does the bare bones of setting up a test environment. First, it will contain the necessary imports. */ ` const suiteTestEnv = ` // +kubebuilder:docs-gen:collapse=Imports /* Now, let's go through the code generated. */ var ( ctx context.Context cancel context.CancelFunc testEnv *envtest.Environment cfg *rest.Config k8sClient client.Client // You'll be using this client in your tests. ) ` const suiteTestAddSchema = ` /* The CronJob Kind is added to the runtime scheme used by the test environment. This ensures that the CronJob API is registered with the scheme, allowing the test controller to recognize and interact with CronJob resources. */ err = batchv1.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) /* After the schemas, you will see the following marker. This marker is what allows new schemas to be added here automatically when a new API is added to the project. */ // +kubebuilder:scaffold:scheme /* The envtest environment is configured to load Custom Resource Definitions (CRDs) from the specified directory. This setup enables the test environment to recognize and interact with the custom resources defined by these CRDs. */` const suiteTestClient = ` /* A client is created for our test CRUD operations. */` const suiteTestDescription = ` /* One thing that this autogenerated file is missing, however, is a way to actually start your controller. The code above will set up a client for interacting with your custom Kind, but will not be able to test your controller behavior. If you want to test your custom controller logic, you’ll need to add some familiar-looking manager logic to your BeforeSuite() function, so you can register your custom controller to run on this test cluster. You may notice that the code below runs your controller with nearly identical logic to your CronJob project’s main.go! The only difference is that the manager is started in a separate goroutine so it does not block the cleanup of envtest when you’re done running your tests. Note that we set up both a "live" k8s client and a separate client from the manager. This is because when making assertions in tests, you generally want to assert against the live state of the API server. If you use the client from the manager (` + "`" + `k8sManager.GetClient` + "`" + `), you'd end up asserting against the contents of the cache instead, which is slower and can introduce flakiness into your tests. We could use the manager's ` + "`" + `APIReader` + "`" + ` to accomplish the same thing, but that would leave us with two clients in our test assertions and setup (one for reading, one for writing), and it'd be easy to make mistakes. Note that we keep the reconciler running against the manager's cache client, though -- we want our controller to behave as it would in production, and we use features of the cache (like indices) in our controller which aren't available when talking directly to the API server. */ k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ Scheme: scheme.Scheme, }) Expect(err).ToNot(HaveOccurred()) err = (&CronJobReconciler{ Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) Expect(err).ToNot(HaveOccurred(), "failed to run manager") }() ` const suiteTestCleanup = ` /* Kubebuilder also generates boilerplate functions for cleaning up envtest and actually running your test files in your controllers/ directory. You won't need to touch these. */ var _ = AfterSuite(func() { By("tearing down the test environment") cancel() Eventually(func() error { return testEnv.Stop() }, time.Minute, time.Second).Should(Succeed()) }) /* Now that you have your controller running on a test cluster and a client ready to perform operations on your CronJob, we can start writing integration tests! */ ` ================================================ FILE: hack/docs/internal/getting-started/generate_getting_started.go ================================================ /* Copyright 2024 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 gettingstarted import ( "log/slog" "os/exec" "path/filepath" hackutils "sigs.k8s.io/kubebuilder/v4/hack/docs/internal/utils" pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" ) // Sample define the sample which will be scaffolded type Sample struct { ctx *utils.TestContext } // NewSample create a new instance of the getting started sample and configure the KB CLI that will be used func NewSample(binaryPath, samplePath string) Sample { slog.Info("Generating the sample context of getting-started...") ctx := hackutils.NewSampleContext(binaryPath, samplePath, "GO111MODULE=on") return Sample{&ctx} } // UpdateTutorial the getting started sample tutorial with the scaffold changes func (sp *Sample) UpdateTutorial() { sp.updateAPI() sp.updateSample() sp.updateController() sp.updateControllerTest() } func (sp *Sample) updateControllerTest() { file := "internal/controller/memcached_controller_test.go" err := pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, file), ". \"github.com/onsi/gomega\"", `. "github.com/onsi/gomega" "k8s.io/utils/ptr" appsv1 "k8s.io/api/apps/v1"`, ) hackutils.CheckError("add imports apis", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, file), "// TODO(user): Specify other spec details if needed.", `Spec: cachev1alpha1.MemcachedSpec{ Size: ptr.To(int32(1)), },`, ) hackutils.CheckError("add spec apis", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, file), `// TODO(user): Add more specific assertions depending on your controller's reconciliation logic. // Example: If you expect a certain status condition after reconciliation, verify it here.`, `By("Checking if Deployment was successfully created in the reconciliation") Eventually(func(g Gomega) { found := &appsv1.Deployment{} g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed()) }).Should(Succeed()) By("Reconciling the custom resource again") _, err = controllerReconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) By("Checking the latest Status Condition added to the Memcached instance") Expect(k8sClient.Get(ctx, typeNamespacedName, memcached)).To(Succeed()) var conditions []metav1.Condition Expect(memcached.Status.Conditions).To(ContainElement( HaveField("Type", Equal(typeAvailableMemcached)), &conditions)) Expect(conditions).To(HaveLen(1), "Multiple conditions of type %s", typeAvailableMemcached) Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "condition %s", typeAvailableMemcached) Expect(conditions[0].Reason).To(Equal("Reconciling"), "condition %s", typeAvailableMemcached)`, ) hackutils.CheckError("add spec apis", err) } func (sp *Sample) updateAPI() { var err error path := "api/v1alpha1/memcached_types.go" err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `limitations under the License. */`, ` // +kubebuilder:docs-gen:collapse=Apache License `) hackutils.CheckError("collapse license in memcached api", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `Any new fields you add must have json tags for the fields to be serialized. `, ` // +kubebuilder:docs-gen:collapse=Imports `) hackutils.CheckError("collapse imports in memcached api", err) err = pluginutil.ReplaceInFile(filepath.Join(sp.ctx.Dir, path), oldSpecAPI, newSpecAPI) hackutils.CheckError("replace spec api", err) } func (sp *Sample) updateSample() { file := filepath.Join(sp.ctx.Dir, "config/samples/cache_v1alpha1_memcached.yaml") err := pluginutil.ReplaceInFile(file, "# TODO(user): Add fields here", sampleSizeFragment) hackutils.CheckError("update sample to add size", err) } func (sp *Sample) updateController() { pathFile := "internal/controller/memcached_controller.go" err := pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, pathFile), "\"context\"", controllerImports, ) hackutils.CheckError("add imports", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, pathFile), "cachev1alpha1 \"example.com/memcached/api/v1alpha1\"\n)", controllerStatusTypes, ) hackutils.CheckError("add status types", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, pathFile), controllerInfoReconcileOld, controllerInfoReconcileNew, ) hackutils.CheckError("add status types", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, pathFile), "// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update", ` // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch`, ) hackutils.CheckError("add markers", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, pathFile), "_ = logf.FromContext(ctx)", "log := logf.FromContext(ctx)", ) hackutils.CheckError("add log var", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, pathFile), "// TODO(user): your logic here", controllerReconcileImplementation, ) hackutils.CheckError("add reconcile implementation", err) err = pluginutil.AppendCodeIfNotExist( filepath.Join(sp.ctx.Dir, pathFile), controllerDeploymentFunc, ) hackutils.CheckError("add func to create Deployment", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, pathFile), "For(&cachev1alpha1.Memcached{}).", ` Owns(&appsv1.Deployment{}).`, ) hackutils.CheckError("add reconcile implementation", err) } // Prepare the Context for the sample project func (sp *Sample) Prepare() { slog.Info("Destroying directory for getting-started sample project") sp.ctx.Destroy() slog.Info("Refreshing tools and creating directory...") err := sp.ctx.Prepare() hackutils.CheckError("Creating directory for sample project", err) } // GenerateSampleProject will generate the sample func (sp *Sample) GenerateSampleProject() { slog.Info("Initializing the getting started project") err := sp.ctx.Init( "--domain", "example.com", "--repo", "example.com/memcached", "--license", "apache2", "--owner", "The Kubernetes authors", ) hackutils.CheckError("Initializing the getting started project", err) slog.Info("Adding a new config type") err = sp.ctx.CreateAPI( "--group", "cache", "--version", "v1alpha1", "--kind", "Memcached", "--resource", "--controller", ) hackutils.CheckError("Creating the API", err) slog.Info("Adding AutoUpdate Plugin") err = sp.ctx.Edit( "--plugins", "autoupdate/v1-alpha", ) hackutils.CheckError("Initializing the getting started project", err) } // CodeGen will call targets to generate code func (sp *Sample) CodeGen() { cmd := exec.Command("go", "mod", "tidy") _, err := sp.ctx.Run(cmd) hackutils.CheckError("Failed to run go mod tidy all for getting started tutorial", err) cmd = exec.Command("make", "all") _, err = sp.ctx.Run(cmd) hackutils.CheckError("Failed to run make all for getting started tutorial", err) cmd = exec.Command("make", "build-installer") _, err = sp.ctx.Run(cmd) hackutils.CheckError("Failed to run make build-installer for getting started tutorial", err) err = sp.ctx.EditHelmPlugin() hackutils.CheckError("Failed to enable helm plugin", err) } const ( oldSpecAPI = "// foo is an example field of Memcached. Edit memcached_types.go to remove/update\n\t// +optional\n\tFoo *string `json:\"foo,omitempty\"`" newSpecAPI = `// size defines the number of Memcached instances // The following markers will use OpenAPI v3 schema to validate the value // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html // +kubebuilder:validation:Minimum=1 // +kubebuilder:validation:Maximum=3 // +kubebuilder:validation:ExclusiveMaximum=false // +optional Size *int32 ` + "`json:\"size,omitempty\"`" ) const sampleSizeFragment = `# TODO(user): edit the following value to ensure the number # of Pods/Instances your Operand must have on cluster size: 1` const controllerImports = `"context" "fmt" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" 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/types" "k8s.io/utils/ptr" ` const controllerStatusTypes = ` // Definitions to manage status conditions const ( // typeAvailableMemcached represents the status of the Deployment reconciliation typeAvailableMemcached = "Available" )` const controllerInfoReconcileOld = `// TODO(user): Modify the Reconcile function to compare the state specified by // the Memcached object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user.` const controllerInfoReconcileNew = `// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator // pattern you will create Controllers which provide a reconcile function // responsible for synchronizing resources until the desired state is reached on the cluster. // Breaking this recommendation goes against the design principles of controller-runtime. // and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention. // For further info: // - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ // - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/` const controllerReconcileImplementation = `// Fetch the Memcached instance // The purpose is check if the Custom Resource for the Kind Memcached // is applied on the cluster if not we return nil to stop the reconciliation memcached := &cachev1alpha1.Memcached{} err := r.Get(ctx, req.NamespacedName, memcached) if err != nil { if apierrors.IsNotFound(err) { // If the custom resource is not found then it usually means that it was deleted or not created // In this way, we will stop the reconciliation log.Info("Memcached resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get memcached") return ctrl.Result{}, err } // Let's just set the status as Unknown when no status is available if len(memcached.Status.Conditions) == 0 { meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) if err = r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } // Let's re-fetch the memcached Custom Resource after updating the status // so that we have the latest state of the resource on the cluster and we will avoid // raising the error "the object has been modified, please apply // your changes to the latest version and try again" which would re-trigger the reconciliation // if we try to update it again in the following operations if err := r.Get(ctx, req.NamespacedName, memcached); err != nil { log.Error(err, "Failed to re-fetch memcached") return ctrl.Result{}, err } } // Check if the deployment already exists, if not create a new one found := &appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found) if err != nil && apierrors.IsNotFound(err) { // Define a new deployment dep, err := r.deploymentForMemcached(memcached) if err != nil { log.Error(err, "Failed to define new Deployment resource for Memcached") // The following implementation will update the status meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionFalse, Reason: "Reconciling", Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", memcached.Name, err)}) if err := r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } return ctrl.Result{}, err } log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) if err = r.Create(ctx, dep); err != nil { log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) return ctrl.Result{}, err } // Deployment created successfully // We will requeue the reconciliation so that we can ensure the state // and move forward for the next operations return ctrl.Result{RequeueAfter: time.Minute}, nil } else if err != nil { log.Error(err, "Failed to get Deployment") // Let's return the error for the reconciliation be re-trigged again return ctrl.Result{}, err } // If the size is not defined in the Custom Resource then we will set the desired replicas to 0 var desiredReplicas int32 = 0 if memcached.Spec.Size != nil { desiredReplicas = *memcached.Spec.Size } // The CRD API defines that the Memcached type have a MemcachedSpec.Size field // to set the quantity of Deployment instances to the desired state on the cluster. // Therefore, the following code will ensure the Deployment size is the same as defined // via the Size spec of the Custom Resource which we are reconciling. if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas { found.Spec.Replicas = ptr.To(desiredReplicas) if err = r.Update(ctx, found); err != nil { log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) // Re-fetch the memcached Custom Resource before updating the status // so that we have the latest state of the resource on the cluster and we will avoid // raising the error "the object has been modified, please apply // your changes to the latest version and try again" which would re-trigger the reconciliation if err := r.Get(ctx, req.NamespacedName, memcached); err != nil { log.Error(err, "Failed to re-fetch memcached") return ctrl.Result{}, err } // The following implementation will update the status meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionFalse, Reason: "Resizing", Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", memcached.Name, err)}) if err := r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err } return ctrl.Result{}, err } // Now, that we update the size we want to requeue the reconciliation // so that we can ensure that we have the latest state of the resource before // update. Also, it will help ensure the desired state on the cluster return ctrl.Result{Requeue: true}, nil } // The following implementation will update the status meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionTrue, Reason: "Reconciling", Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", memcached.Name, desiredReplicas)}) if err := r.Status().Update(ctx, memcached); err != nil { log.Error(err, "Failed to update Memcached status") return ctrl.Result{}, err }` const controllerDeploymentFunc = `// deploymentForMemcached returns a Memcached Deployment object func (r *MemcachedReconciler) deploymentForMemcached( memcached *cachev1alpha1.Memcached) (*appsv1.Deployment, error) { image := "memcached:1.6.26-alpine3.19" dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: memcached.Name, Namespace: memcached.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: memcached.Spec.Size, Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"app.kubernetes.io/name": "project"}, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"app.kubernetes.io/name": "project"}, }, Spec: corev1.PodSpec{ SecurityContext: &corev1.PodSecurityContext{ RunAsNonRoot: ptr.To(true), SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, Containers: []corev1.Container{{ Image: image, Name: "memcached", ImagePullPolicy: corev1.PullIfNotPresent, // Ensure restrictive context for the container // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), RunAsUser: ptr.To(int64(1001)), AllowPrivilegeEscalation: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", }, }, }, Ports: []corev1.ContainerPort{{ ContainerPort: 11211, Name: "memcached", }}, Command: []string{"memcached", "--memory-limit=64", "-o", "modern", "-v"}, }}, }, }, }, } // Set the ownerRef for the Deployment // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil { return nil, err } return dep, nil }` ================================================ FILE: hack/docs/internal/multiversion-tutorial/controller_tests_code.go ================================================ /* Copyright 2023 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 multiversion const multiversionControllerTest = `/* 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 controller import ( "context" "fmt" "reflect" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" cronjobv1 "tutorial.kubebuilder.io/project/api/v1" ) var _ = Describe("CronJob controller", func() { Context("CronJob controller test", func() { const NamespaceName = "test-cronjob" ctx := context.Background() namespace := &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: NamespaceName, Namespace: NamespaceName, }, } SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) BeforeEach(func() { By("Creating the Namespace to perform the tests") err := k8sClient.Get(ctx, types.NamespacedName{Name: NamespaceName}, &v1.Namespace{}) if err != nil && errors.IsNotFound(err) { err = k8sClient.Create(ctx, namespace) Expect(err).NotTo(HaveOccurred()) } }) AfterEach(func() { // Note: We don't delete the namespace here to avoid issues with parallel test execution. // The namespace will be cleaned up when the test suite finishes. }) It("should initialize status conditions on first reconciliation", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Checking that status conditions are initialized") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Conditions).NotTo(BeEmpty()) }).Should(Succeed()) By("Cleaning up the CronJob") Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should set AllJobsCompleted condition when no active jobs exist", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Checking that the CronJob has zero active Jobs") Consistently(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(BeEmpty()) }).WithTimeout(time.Second * 5).WithPolling(time.Millisecond * 250).Should(Succeed()) By("Checking AllJobsCompleted condition") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) if len(availableConditions) > 0 { Expect(availableConditions[0].Status).To(Equal(metav1.ConditionTrue)) Expect(availableConditions[0].Reason).To(Equal("AllJobsCompleted")) } var progressingConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Progressing")), &progressingConditions)) if len(progressingConditions) > 0 { Expect(progressingConditions[0].Status).To(Equal(metav1.ConditionFalse)) Expect(progressingConditions[0].Reason).To(Equal("NoJobsActive")) } By("Cleaning up the CronJob") Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should track active jobs and set JobsActive condition", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Creating an active Job owned by the CronJob") testJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("test-job-%d", GinkgoRandomSeed()), Namespace: NamespaceName, }, Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, } kind := reflect.TypeFor[cronjobv1.CronJob]().Name() gvk := cronjobv1.GroupVersion.WithKind(kind) Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) }).Should(Succeed()) controllerRef := metav1.NewControllerRef(cronJob, gvk) testJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) Expect(k8sClient.Create(ctx, testJob)).To(Succeed()) testJob.Status.Active = 2 Expect(k8sClient.Status().Update(ctx, testJob)).To(Succeed()) By("Checking that the CronJob has one active Job in status") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) g.Expect(cronJob.Status.Active).To(HaveLen(1)) g.Expect(cronJob.Status.Active[0].Name).To(Equal(testJob.Name)) }).Should(Succeed()) By("Checking JobsActive conditions") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) Expect(availableConditions).To(HaveLen(1)) Expect(availableConditions[0].Status).To(Equal(metav1.ConditionTrue)) Expect(availableConditions[0].Reason).To(Equal("JobsActive")) var progressingConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Progressing")), &progressingConditions)) Expect(progressingConditions).To(HaveLen(1)) Expect(progressingConditions[0].Status).To(Equal(metav1.ConditionTrue)) Expect(progressingConditions[0].Reason).To(Equal("JobsActive")) By("Cleaning up") Expect(k8sClient.Delete(ctx, testJob)).To(Succeed()) Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should set Degraded condition when jobs fail", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Creating a failed Job owned by the CronJob") failedJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("test-job-failed-%d", GinkgoRandomSeed()), Namespace: NamespaceName, }, Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, } kind := reflect.TypeFor[cronjobv1.CronJob]().Name() gvk := cronjobv1.GroupVersion.WithKind(kind) Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) }).Should(Succeed()) controllerRef := metav1.NewControllerRef(cronJob, gvk) failedJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef}) Expect(k8sClient.Create(ctx, failedJob)).To(Succeed()) now := metav1.Now() failedJob.Status.StartTime = &now failedJob.Status.Conditions = append(failedJob.Status.Conditions, batchv1.JobCondition{ Type: batchv1.JobFailureTarget, Status: v1.ConditionTrue, }, batchv1.JobCondition{ Type: batchv1.JobFailed, Status: v1.ConditionTrue, }) Expect(k8sClient.Status().Update(ctx, failedJob)).To(Succeed()) By("Checking that Degraded=True when jobs fail") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var degradedConditions []metav1.Condition g.Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Degraded")), °radedConditions)) if len(degradedConditions) > 0 { g.Expect(degradedConditions[0].Status).To(Equal(metav1.ConditionTrue)) g.Expect(degradedConditions[0].Reason).To(Equal("JobsFailed")) } }).Should(Succeed()) By("Checking that Available=False when jobs fail") Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) if len(availableConditions) > 0 { Expect(availableConditions[0].Status).To(Equal(metav1.ConditionFalse)) Expect(availableConditions[0].Reason).To(Equal("JobsFailed")) } By("Cleaning up") Expect(k8sClient.Delete(ctx, failedJob)).To(Succeed()) Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) It("should set Available=False when CronJob is suspended", func() { cronJobName := fmt.Sprintf("test-cronjob-%d", GinkgoRandomSeed()) typeNamespacedName := types.NamespacedName{ Name: cronJobName, Namespace: NamespaceName, } cronJob := &cronjobv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: cronJobName, Namespace: NamespaceName, }, Spec: cronjobv1.CronJobSpec{ Schedule: "1 * * * *", JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: v1.PodTemplateSpec{ Spec: v1.PodSpec{ Containers: []v1.Container{ { Name: "test-container", Image: "test-image", }, }, RestartPolicy: v1.RestartPolicyOnFailure, }, }, }, }, }, } Expect(k8sClient.Create(ctx, cronJob)).To(Succeed()) By("Updating the CronJob to suspend it") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) cronJob.Spec.Suspend = ptr.To(true) g.Expect(k8sClient.Update(ctx, cronJob)).To(Succeed()) }).Should(Succeed()) By("Checking that Available=False when suspended") Eventually(func(g Gomega) { g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed()) var availableConditions []metav1.Condition g.Expect(cronJob.Status.Conditions).To(ContainElement( HaveField("Type", Equal("Available")), &availableConditions)) if len(availableConditions) > 0 { g.Expect(availableConditions[0].Status).To(Equal(metav1.ConditionFalse)) g.Expect(availableConditions[0].Reason).To(Equal("Suspended")) } }).Should(Succeed()) By("Cleaning up the CronJob") Expect(k8sClient.Delete(ctx, cronJob)).To(Succeed()) }) }) }) ` ================================================ FILE: hack/docs/internal/multiversion-tutorial/cronjob_v1.go ================================================ /* Copyright 2023 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 multiversion const cronjobSpecComment = `/* First, let's take a look at our spec. As we discussed before, spec holds *desired state*, so any "inputs" to our controller go here. Fundamentally a CronJob needs the following pieces: - A schedule (the *cron* in CronJob) - A template for the Job to run (the *job* in CronJob) We'll also want a few extras, which will make our users' lives easier: - A deadline for starting jobs (if we miss this deadline, we'll just wait till the next scheduled time) - What to do if multiple jobs would run at once (do we wait? stop the old one? run both?) - A way to pause the running of a CronJob, in case something's wrong with it - Limits on old job history Remember, since we never read our own status, we need to have some other way to keep track of whether a job has run. We can use at least one old job to do this. We'll use several markers (` + "`// +comment`" + `) to specify additional metadata. These will be used by [controller-tools](https://github.com/kubernetes-sigs/controller-tools) when generating our CRD manifest. As we'll see in a bit, controller-tools will also use GoDoc to form descriptions for the fields. */` const concurrencyPolicyComment = `/* We define a custom type to hold our concurrency policy. It's actually just a string under the hood, but the type gives extra documentation, and allows us to attach validation on the type instead of the field, making the validation more easily reusable. */` const statusDesignComment = `/* Next, let's design our status, which holds observed state. It contains any information we want users or other controllers to be able to easily obtain. We'll keep a list of actively running jobs, as well as the last time that we successfully ran our job. Notice that we use ` + "`metav1.Time`" + ` instead of ` + "`time.Time`" + ` to get the stable serialization, as mentioned above. */` const boilerplateReplacement = `// +kubebuilder:docs-gen:collapse=Remaining code from cronjob_types.go /* Since we'll have more than one version, we'll need to mark a storage version. This is the version that the Kubernetes API server uses to store our data. We'll chose the v1 version for our project. We'll use the [` + "`+kubebuilder:storageversion`" + `](/reference/markers/crd.md) to do this. Note that multiple versions may exist in storage if they were written before the storage version changes -- changing the storage version only affects how objects are created/updated after the change. */ // +kubebuilder:object:root=true // +kubebuilder:storageversion` ================================================ FILE: hack/docs/internal/multiversion-tutorial/cronjob_v2.go ================================================ /* Copyright 2023 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 multiversion const importV2 = `import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" )` const importReplacement = `/* */ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" )` // FIXME: We should just insert and replace what is need and not a block of code in this way const cronjobSpecMore = `// startingDeadlineSeconds defines in seconds for starting the job if it misses scheduled // time for any reason. Missed jobs executions will be counted as failed ones. // +optional // +kubebuilder:validation:Minimum=0 StartingDeadlineSeconds *int64 ` + "`json:\"startingDeadlineSeconds,omitempty\"`" + ` // concurrencyPolicy defines how to treat concurrent executions of a Job. // Valid values are: // - "Allow" (default): allows CronJobs to run concurrently; // - "Forbid": forbids concurrent runs, skipping next run if previous run hasn't finished yet; // - "Replace": cancels currently running job and replaces it with a new one // +optional // +kubebuilder:default:=Allow ConcurrencyPolicy ConcurrencyPolicy ` + "`json:\"concurrencyPolicy,omitempty\"`" + ` // suspend tells the controller to suspend subsequent executions, it does // not apply to already started executions. Defaults to false. // +optional Suspend *bool ` + "`json:\"suspend,omitempty\"`" + ` // jobTemplate defines the job that will be created when executing a CronJob. // +required JobTemplate batchv1.JobTemplateSpec ` + "`json:\"jobTemplate\"`" + ` // successfulJobsHistoryLimit defines the number of successful finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 SuccessfulJobsHistoryLimit *int32 ` + "`json:\"successfulJobsHistoryLimit,omitempty\"`" + ` // failedJobsHistoryLimit defines the number of failed finished jobs to retain. // This is a pointer to distinguish between explicit zero and not specified. // +optional // +kubebuilder:validation:Minimum=0 FailedJobsHistoryLimit *int32 ` + "`json:\"failedJobsHistoryLimit,omitempty\"`" + ` } // +kubebuilder:docs-gen:collapse=CronJobSpec Full Code /* Next, we'll need to define a type to hold our schedule. Based on our proposed YAML above, it'll have a field for each corresponding Cron "field". */ // describes a Cron schedule. type CronSchedule struct { // minute specifies the minutes during which the job executes. // +optional Minute *CronField ` + "`json:\"minute,omitempty\"`" + ` // hour specifies the hour during which the job executes. // +optional Hour *CronField ` + "`json:\"hour,omitempty\"`" + ` // dayOfMonth specifies the day of the month during which the job executes. // +optional DayOfMonth *CronField ` + "`json:\"dayOfMonth,omitempty\"`" + ` // month specifies the month during which the job executes. // +optional Month *CronField ` + "`json:\"month,omitempty\"`" + ` // dayOfWeek specifies the day of the week during which the job executes. // +optional DayOfWeek *CronField ` + "`json:\"dayOfWeek,omitempty\"`" + ` } /* Finally, we'll define a wrapper type to represent a field. We could attach additional validation to this field, but for now we'll just use it for documentation purposes. */ // represents a Cron field specifier. type CronField string /* All the other types will stay the same as before. */ // ConcurrencyPolicy describes how the job will be handled. // Only one of the following concurrent policies may be specified. // If none of the following policies is specified, the default one // is AllowConcurrent. // +kubebuilder:validation:Enum=Allow;Forbid;Replace type ConcurrencyPolicy string const ( // AllowConcurrent allows CronJobs to run concurrently. AllowConcurrent ConcurrencyPolicy = "Allow" // ForbidConcurrent forbids concurrent runs, skipping next run if previous // hasn't finished yet. ForbidConcurrent ConcurrencyPolicy = "Forbid" // ReplaceConcurrent cancels currently running job and replaces it with a new one. ReplaceConcurrent ConcurrencyPolicy = "Replace" ) ` ================================================ FILE: hack/docs/internal/multiversion-tutorial/generate_multiversion.go ================================================ /* Copyright 2023 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 multiversion import ( log "log/slog" "os/exec" "path/filepath" "github.com/spf13/afero" hackutils "sigs.k8s.io/kubebuilder/v4/hack/docs/internal/utils" pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" ) // Sample define the sample which will be scaffolded type Sample struct { ctx *utils.TestContext } // NewSample create a new instance of the sample and configure the KB CLI that will be used func NewSample(binaryPath, samplePath string) Sample { log.Info("Generating the sample context of MultiVersion Cronjob...") ctx := hackutils.NewSampleContext(binaryPath, samplePath, "GO111MODULE=on") return Sample{&ctx} } // Prepare the Context for the sample project func (sp *Sample) Prepare() { log.Info("refreshing tools and creating directory for multiversion ...") err := sp.ctx.Prepare() hackutils.CheckError("creating directory for multiversion project", err) } // GenerateSampleProject will generate the sample func (sp *Sample) GenerateSampleProject() { log.Info("Initializing the multiversion cronjob project") log.Info("Creating v2 API") err := sp.ctx.CreateAPI( "--group", "batch", "--version", "v2", "--kind", "CronJob", "--resource=true", "--controller=false", ) hackutils.CheckError("Creating the v2 API without controller", err) log.Info("Creating conversion webhook for v1") err = sp.ctx.CreateWebhook( "--group", "batch", "--version", "v1", "--kind", "CronJob", "--conversion", "--spoke", "v2", ) hackutils.CheckError("Creating conversion webhook for v1", err) log.Info("Creating defaulting and validation webhook for v2") err = sp.ctx.CreateWebhook( "--group", "batch", "--version", "v2", "--kind", "CronJob", "--defaulting", "--programmatic-validation", ) hackutils.CheckError("Creating defaulting and validation webhook for v2", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), `// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods, // as this struct is used only for temporary operations and does not need to be deeply copied. type CronJobCustomValidator struct {`, `// +kubebuilder:docs-gen:collapse=Remaining Webhook Code`) hackutils.CheckError("adding marker collapse", err) } // UpdateTutorial the muilt-version sample tutorial with the scaffold changes func (sp *Sample) UpdateTutorial() { log.Info("Update tutorial with multiversion code") // Update files according to the multiversion sp.updateCronjobV1ForConversion() sp.updateAPIV1() sp.updateAPIV2() sp.updateWebhookV2() path := "internal/webhook/v1/cronjob_webhook_test.go" err := pluginutil.InsertCode(filepath.Join(sp.ctx.Dir, path), `// TODO (user): Add any additional imports if needed`, ` "k8s.io/utils/ptr"`) hackutils.CheckError("add import for webhook tests", err) sp.updateConversionFiles() sp.updateSampleV2() sp.updateMain() sp.updateControllerTest() sp.updateE2EWebhookConversion() } func (sp *Sample) updateControllerTest() { testContent := []byte(multiversionControllerTest) fs := afero.NewOsFs() testPath := filepath.Join(sp.ctx.Dir, "internal/controller/cronjob_controller_test.go") err := afero.WriteFile(fs, testPath, testContent, 0o600) hackutils.CheckError("replacing controller test for multiversion", err) } func (sp *Sample) updateCronjobV1ForConversion() { path := "internal/webhook/v1/cronjob_webhook.go" err := pluginutil.ReplaceInFile(filepath.Join(sp.ctx.Dir, path), "Then, we set up the webhook with the manager.", `This setup doubles as setup for our conversion webhooks: as long as our types implement the [Hub](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub) and [Convertible](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) interfaces, a conversion webhook will be registered. `) hackutils.CheckError("manager fix doc comment", err) err = pluginutil.ReplaceInFile(filepath.Join(sp.ctx.Dir, "internal/webhook/v1/cronjob_webhook.go"), "// +kubebuilder:docs-gen:collapse=validateCronJobName() Code Implementation", ``) hackutils.CheckError("removing collapse valida for cronjob tutorial", err) } func (sp *Sample) updateSampleV2() { path := filepath.Join(sp.ctx.Dir, "config/samples/batch_v2_cronjob.yaml") oldText := `# TODO(user): Add fields here` err := pluginutil.ReplaceInFile( path, oldText, sampleV2Code, ) hackutils.CheckError("replacing TODO with sampleV2Code in batch_v2_cronjob.yaml", err) } func (sp *Sample) updateConversionFiles() { path := filepath.Join(sp.ctx.Dir, "api/v1/cronjob_conversion.go") err := pluginutil.InsertCodeIfNotExist(path, "limitations under the License.\n*/", "\n// +kubebuilder:docs-gen:collapse=Apache License") hackutils.CheckError("appending into hub v1 collapse docs", err) err = pluginutil.ReplaceInFile(path, "// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!", hubV1CodeComment) hackutils.CheckError("adding comment to hub v1", err) path = filepath.Join(sp.ctx.Dir, "api/v2/cronjob_conversion.go") err = pluginutil.InsertCodeIfNotExist(path, "limitations under the License.\n*/", "\n// +kubebuilder:docs-gen:collapse=Apache License") hackutils.CheckError("appending into hub v2 collapse docs", err) err = pluginutil.InsertCode(path, "import (", ` "fmt" "strings" `) hackutils.CheckError("adding imports to hub v2", err) err = pluginutil.InsertCodeIfNotExist(path, "batchv1 \"tutorial.kubebuilder.io/project/api/v1\"\n)", `// +kubebuilder:docs-gen:collapse=Imports /* Our "spoke" versions need to implement the [`+"`"+`Convertible`+"`"+`](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Convertible) interface. Namely, they'll need `+"`"+`ConvertTo()`+"`"+` and `+"`"+`ConvertFrom()`+"`"+` methods to convert to/from the hub version. */ `) hackutils.CheckError("appending into hub v2 collapse docs", err) err = pluginutil.ReplaceInFile(path, "package v2", hubV2CodeComment) hackutils.CheckError("adding comment to hub v2", err) err = pluginutil.ReplaceInFile(path, `// TODO(user): Implement conversion logic from v2 to v1 // Example: Copying Spec fields // dst.Spec.Size = src.Spec.Replicas // Copy ObjectMeta to preserve name, namespace, labels, etc. dst.ObjectMeta = src.ObjectMeta return nil }`, hubV2CovertTo) hackutils.CheckError("replace covertTo at hub v2", err) err = pluginutil.ReplaceInFile(path, `// TODO(user): Implement conversion logic from v1 to v2 // Example: Copying Spec fields // dst.Spec.Replicas = src.Spec.Size // Copy ObjectMeta to preserve name, namespace, labels, etc. dst.ObjectMeta = src.ObjectMeta return nil } `, hubV2ConvertFromCode) hackutils.CheckError("replace covert from at hub v2", err) err = pluginutil.ReplaceInFile(path, "// ConvertFrom converts the Hub version (v1) to this CronJob (v2).", `/* ConvertFrom is expected to modify its receiver to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ // ConvertFrom converts the Hub version (v1) to this CronJob (v2).`) hackutils.CheckError("replace covert from info at hub v2", err) err = pluginutil.ReplaceInFile(path, "// ConvertTo converts this CronJob (v2) to the Hub version (v1).", `/* ConvertTo is expected to modify its argument to contain the converted object. Most of the conversion is straightforward copying, except for converting our changed field. */ // ConvertTo converts this CronJob (v2) to the Hub version (v1).`) hackutils.CheckError("replace covert info at hub v2", err) } func (sp *Sample) updateAPIV1() { path := "api/v1/cronjob_types.go" err := pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `// +kubebuilder:subresource:status `, `// +versionName=v1 // +kubebuilder:storageversion`, ) hackutils.CheckError("add version and marker for storage version", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), cronjobSpecComment, "", ) hackutils.CheckError("removing spec explanation", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `type CronJob struct { /* */`, `type CronJob struct {`, ) hackutils.CheckError("removing comment empty from struct", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `/* Finally, we have the rest of the boilerplate that we've already discussed. As previously noted, we don't need to change this, except to mark that we want a status subresource, so that we behave like built-in kubernetes types. */`, ``, ) hackutils.CheckError("removing comment from cronjob tutorial", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// +kubebuilder:object:root=true // CronJobList contains a list of CronJob`, `/* */ // +kubebuilder:object:root=true // CronJobList contains a list of CronJob`, ) hackutils.CheckError("add comment empty after struct", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), concurrencyPolicyComment, "", ) hackutils.CheckError("removing concurrency policy explanation", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), statusDesignComment, "", ) hackutils.CheckError("removing status design explanation", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// +kubebuilder:object:root=true // +kubebuilder:storageversion`, boilerplateReplacement, ) hackutils.CheckError("add comment with storage version explanation", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, "api/v1/cronjob_types.go"), `// +kubebuilder:docs-gen:collapse=Root Object Definitions`, `// +kubebuilder:docs-gen:collapse=Remaining code from cronjob_types.go`, ) hackutils.CheckError("replacing docs-gen collapse comment", err) } func (sp *Sample) updateWebhookV2() { path := "internal/webhook/v2/cronjob_webhook.go" err := pluginutil.InsertCodeIfNotExist( filepath.Join(sp.ctx.Dir, path), "limitations under the License.\n*/", "\n// +kubebuilder:docs-gen:collapse=Apache License") hackutils.CheckError("adding Apache License collapse marker to webhook v2", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `import ( "context"`, ` "strings" "github.com/robfig/cron" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" validationutils "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field"`, ) hackutils.CheckError("replacing imports in v2", err) // Add collapse marker after the import block to hide imports err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `batchv2 "tutorial.kubebuilder.io/project/api/v2" )`, ` // +kubebuilder:docs-gen:collapse=Imports`) hackutils.CheckError("adding imports collapse marker to webhook v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// TODO(user): Add more fields as needed for defaulting`, cronJobFieldsForDefaulting, ) hackutils.CheckError("replacing defaulting fields in v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// TODO(user): fill in your defaulting logic. return nil`, cronJobDefaultingLogic, ) hackutils.CheckError("replacing defaulting logic in v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// TODO(user): fill in your validation logic upon object creation. return nil, nil`, `return nil, validateCronJob(obj)`, ) hackutils.CheckError("replacing validation logic for creation in v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// TODO(user): fill in your validation logic upon object update. return nil, nil`, `return nil, validateCronJob(newObj)`, ) hackutils.CheckError("replacing validation logic for update in v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), originalSetupManager, replaceSetupManager, ) hackutils.CheckError("replacing SetupWebhookWithManager in v2", err) err = pluginutil.AppendCodeAtTheEnd( filepath.Join(sp.ctx.Dir, path), cronJobDefaultFunction, ) hackutils.CheckError("adding Default function in v2", err) // Add the validateCronJob function err = pluginutil.AppendCodeAtTheEnd( filepath.Join(sp.ctx.Dir, path), cronJobValidationFunction, ) hackutils.CheckError("adding validateCronJob function in v2", err) } func (sp *Sample) updateMain() { path := "cmd/main.go" err := pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `"k8s.io/apimachinery/pkg/runtime"`, `kbatchv1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/runtime"`, ) hackutils.CheckError("add import main.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `utilruntime.Must(batchv1.AddToScheme(scheme))`, `utilruntime.Must(kbatchv1.AddToScheme(scheme)) // we've added this ourselves utilruntime.Must(batchv1.AddToScheme(scheme))`, ) hackutils.CheckError("add schema main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `// +kubebuilder:scaffold:scheme }`, ` // +kubebuilder:docs-gen:collapse=existing setup /* */`, ) hackutils.CheckError("insert doc marker existing setup main.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) } // +kubebuilder:docs-gen:collapse=Remaining code from main.go`, `if err != nil { setupLog.Error(err, "Failed to start manager") os.Exit(1) }`, ) hackutils.CheckError("remove doc marker old staff from main.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `/* The first difference to notice is that kubebuilder has added the new API group's package (`+"`batchv1`"+`) to our scheme. This means that we can use those objects in our controller. If we would be using any other CRD we would have to add their scheme the same way. Builtin types such as Job have their scheme added by `+"`clientgoscheme`"+`. */`, "", ) hackutils.CheckError("remove API group explanation main.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `/* The other thing that's changed is that kubebuilder has added a block calling our CronJob controller's `+"`SetupWithManager`"+` method. */`, "", ) hackutils.CheckError("remove SetupWithManager explanation main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `// +kubebuilder:docs-gen:collapse=Imports`, ` /* */ `, ) hackutils.CheckError("insert comment after import in the main.go", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `if !enableHTTP2 { tlsOpts = append(tlsOpts, disableHTTP2) }`, ` // +kubebuilder:docs-gen:collapse=Manager Setup`, ) hackutils.CheckError("adding manager setup collapse marker main.go", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `/* We'll also set up webhooks for our type, which we'll talk about next. We just need to add them to the manager. Since we might want to run the webhooks separately, or not run them when testing our controller locally, we'll put them behind an environment variable. We'll just make sure to set `+"`ENABLE_WEBHOOKS=false`"+` when we run locally. */`, `/* Our existing call to SetupWebhookWithManager registers our conversion webhooks with the manager, too. */`, ) hackutils.CheckError("replace webhook setup explanation main.go", err) } func (sp *Sample) updateAPIV2() { path := "api/v2/cronjob_types.go" err := pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `// +kubebuilder:subresource:status `, `// +versionName=v2`, ) hackutils.CheckError("add marker version for v2", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), "limitations under the License.\n*/", // This is the anchor point where we want to insert the code ` // +kubebuilder:docs-gen:collapse=Apache License /* Since we're in a v2 package, controller-gen will assume this is for the v2 version automatically. We could override that with the [`+"`+versionName`"+` marker](/reference/markers/crd.md). */`) hackutils.CheckError("insert doc marker for license", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), importV2, importReplacement, ) hackutils.CheckError("replace imports v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// CronJobSpec defines the desired state of CronJob`, `// +kubebuilder:docs-gen:collapse=Imports /* We'll leave our spec largely unchanged, except to change the schedule field to a new type. */ // CronJobSpec defines the desired state of CronJob`, ) hackutils.CheckError("replace doc about CronjobSpec v2", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // The following markers will use OpenAPI v3 schema to validate the value // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html`, `// schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. // +required Schedule CronSchedule `+"`json:\"schedule\"`"+` /* */ `, ) hackutils.CheckError("add new schedule spec type", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `// foo is an example field of CronJob. Edit cronjob_types.go to remove/update // +optional Foo *string `+"`json:\"foo,omitempty\"`", cronjobSpecMore, ) hackutils.CheckError("replace Foo with cronjob spec fields", err) err = pluginutil.ReplaceInFile( filepath.Join(sp.ctx.Dir, path), `) }`, `) `, ) hackutils.CheckError("replace Foo with cronjob spec fields", err) err = pluginutil.InsertCode( filepath.Join(sp.ctx.Dir, path), `type CronJobStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "make" to regenerate code after modifying this file`, ` // active defines a list of pointers to currently running jobs. // +optional // +listType=atomic // +kubebuilder:validation:MinItems=1 // +kubebuilder:validation:MaxItems=10 Active []corev1.ObjectReference `+"`json:\"active,omitempty\"`"+` // lastScheduleTime defines the information when was the last time the job was successfully scheduled. // +optional LastScheduleTime *metav1.Time `+"`json:\"lastScheduleTime,omitempty\"`"+` `) hackutils.CheckError("insert status for cronjob v2", err) err = pluginutil.AppendCodeAtTheEnd( filepath.Join(sp.ctx.Dir, path), ` // +kubebuilder:docs-gen:collapse=Other Types`) hackutils.CheckError("append marker at the end of the docs", err) } // CodeGen will call targets to generate code func (sp *Sample) CodeGen() { cmd := exec.Command("make", "all") _, err := sp.ctx.Run(cmd) hackutils.CheckError("Failed to run make all for multiversion tutorial", err) cmd = exec.Command("make", "build-installer") _, err = sp.ctx.Run(cmd) hackutils.CheckError("Failed to run make build-installer for multiversion tutorial", err) err = sp.ctx.EditHelmPlugin() hackutils.CheckError("Failed to enable helm plugin", err) } const webhookConversionE2ETest = ` It("should successfully convert between v1 and v2 versions", func() { By("waiting for the webhook service to be ready") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "endpoints", "-n", namespace, "-l", "control-plane=controller-manager", "-o", "jsonpath={.items[0].subsets[0].addresses[0].ip}") output, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get webhook service endpoints") g.Expect(strings.TrimSpace(output)).NotTo(BeEmpty(), "Webhook endpoint should have an IP") }, time.Minute, time.Second).Should(Succeed()) By("creating a v1 CronJob with a specific schedule") cmd := exec.Command("kubectl", "apply", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace) _, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create v1 CronJob") By("waiting for the v1 CronJob to be created") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace) output, err := utils.Run(cmd) if err != nil { // Log controller logs on failure for debugging logCmd := exec.Command("kubectl", "logs", "-l", "control-plane=controller-manager", "-n", namespace, "--tail=50") logs, _ := utils.Run(logCmd) _, _ = fmt.Fprintf(GinkgoWriter, "Controller logs when CronJob not found:\n%s\n", logs) } g.Expect(err).NotTo(HaveOccurred(), "v1 CronJob should exist, output: "+output) }, time.Minute, time.Second).Should(Succeed()) By("fetching the v1 CronJob and verifying the schedule format") cmd = exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule}") v1Schedule, err := utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to get v1 CronJob schedule") Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"), "v1 schedule should be in cron format") By("fetching the same CronJob as v2 and verifying the converted schedule") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule.minute}") v2Minute, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule") g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"), "v2 schedule.minute should be converted from v1 schedule") }, time.Minute, time.Second).Should(Succeed()) By("creating a v2 CronJob with structured schedule fields") cmd = exec.Command("kubectl", "apply", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace) _, err = utils.Run(cmd) Expect(err).NotTo(HaveOccurred(), "Failed to create v2 CronJob") By("verifying the v2 CronJob has the correct structured schedule") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.v2.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule.minute}") v2Minute, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get v2 CronJob schedule") g.Expect(strings.TrimSpace(v2Minute)).To(Equal("*/1"), "v2 CronJob should have minute field set") }, time.Minute, time.Second).Should(Succeed()) By("fetching the v2 CronJob as v1 and verifying schedule conversion") Eventually(func(g Gomega) { cmd := exec.Command("kubectl", "get", "cronjob.v1.batch.tutorial.kubebuilder.io", "cronjob-sample", "-n", namespace, "-o", "jsonpath={.spec.schedule}") v1Schedule, err := utils.Run(cmd) g.Expect(err).NotTo(HaveOccurred(), "Failed to get converted v1 schedule") // When v2 only has minute field set, it converts to "*/1 * * * *" g.Expect(strings.TrimSpace(v1Schedule)).To(Equal("*/1 * * * *"), "v1 schedule should be converted from v2 structured schedule") }, time.Minute, time.Second).Should(Succeed()) })` func (sp *Sample) updateE2EWebhookConversion() { cronjobE2ETest := filepath.Join(sp.ctx.Dir, "test", "e2e", "e2e_test.go") // Add strings import if not already present err := pluginutil.InsertCodeIfNotExist(cronjobE2ETest, ` "os/exec" "path/filepath" "time"`, ` "strings"`) hackutils.CheckError("adding strings import for e2e test", err) // Add CronJob cleanup to the AfterEach block err = pluginutil.InsertCode(cronjobE2ETest, ` // After each test, check for failures and collect logs, events, // and pod descriptions for debugging. AfterEach(func() {`, ` By("Cleaning up test CronJob resources") cmd := exec.Command("kubectl", "delete", "-f", "config/samples/batch_v1_cronjob.yaml", "-n", namespace, "--ignore-not-found=true") _, _ = utils.Run(cmd) cmd = exec.Command("kubectl", "delete", "-f", "config/samples/batch_v2_cronjob.yaml", "-n", namespace, "--ignore-not-found=true") _, _ = utils.Run(cmd) `) hackutils.CheckError("adding CronJob cleanup to AfterEach", err) // Add webhook conversion test after the existing TODO comment err = pluginutil.InsertCode(cronjobE2ETest, ` // TODO: Customize the e2e test suite with scenarios specific to your project. // Consider applying sample/CR(s) and check their status and/or verifying // the reconciliation by using the metrics, i.e.: // metricsOutput, err := getMetricsOutput() // Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod") // Expect(metricsOutput).To(ContainSubstring( // fmt.Sprintf(`+"`"+`controller_runtime_reconcile_total{controller="%s",result="success"} 1`+"`"+`, // strings.ToLower(), // ))`, webhookConversionE2ETest) hackutils.CheckError("adding webhook conversion e2e test", err) } ================================================ FILE: hack/docs/internal/multiversion-tutorial/hub.go ================================================ /* Copyright 2023 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 multiversion const hubV1CodeComment = ` /* Implementing the hub method is pretty easy -- we just have to add an empty method called ` + "`" + `Hub()` + "`" + `to serve as a [marker](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc#Hub). We could also just put this inline in our cronjob_types.go file. */ ` const hubV2CodeComment = `package v2 /* For imports, we'll need the controller-runtime [` + "`" + `conversion` + "`" + `](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/conversion?tab=doc) package, plus the API version for our hub type (v1), and finally some of the standard packages. */ ` const hubV2CovertTo = `sched := src.Spec.Schedule scheduleParts := []string{"*", "*", "*", "*", "*"} if sched.Minute != nil { scheduleParts[0] = string(*sched.Minute) } if sched.Hour != nil { scheduleParts[1] = string(*sched.Hour) } if sched.DayOfMonth != nil { scheduleParts[2] = string(*sched.DayOfMonth) } if sched.Month != nil { scheduleParts[3] = string(*sched.Month) } if sched.DayOfWeek != nil { scheduleParts[4] = string(*sched.DayOfWeek) } dst.Spec.Schedule = strings.Join(scheduleParts, " ") /* The rest of the conversion is pretty rote. */ // ObjectMeta dst.ObjectMeta = src.ObjectMeta // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds dst.Spec.ConcurrencyPolicy = batchv1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit // Status dst.Status.Active = src.Status.Active dst.Status.LastScheduleTime = src.Status.LastScheduleTime return nil } // +kubebuilder:docs-gen:collapse=rote conversion` const hubV2ConvertFromCode = `schedParts := strings.Split(src.Spec.Schedule, " ") if len(schedParts) != 5 { return fmt.Errorf("invalid schedule: not a standard 5-field schedule") } partIfNeeded := func(raw string) *CronField { if raw == "*" { return nil } part := CronField(raw) return &part } dst.Spec.Schedule.Minute = partIfNeeded(schedParts[0]) dst.Spec.Schedule.Hour = partIfNeeded(schedParts[1]) dst.Spec.Schedule.DayOfMonth = partIfNeeded(schedParts[2]) dst.Spec.Schedule.Month = partIfNeeded(schedParts[3]) dst.Spec.Schedule.DayOfWeek = partIfNeeded(schedParts[4]) /* The rest of the conversion is pretty rote. */ // ObjectMeta dst.ObjectMeta = src.ObjectMeta // Spec dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds dst.Spec.ConcurrencyPolicy = ConcurrencyPolicy(src.Spec.ConcurrencyPolicy) dst.Spec.Suspend = src.Spec.Suspend dst.Spec.JobTemplate = src.Spec.JobTemplate dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit // Status dst.Status.Active = src.Status.Active dst.Status.LastScheduleTime = src.Status.LastScheduleTime return nil } // +kubebuilder:docs-gen:collapse=rote conversion` ================================================ FILE: hack/docs/internal/multiversion-tutorial/samples.go ================================================ /* Copyright 2022 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 multiversion const sampleV2Code = `schedule: minute: "*/1" startingDeadlineSeconds: 60 concurrencyPolicy: Allow # explicitly specify, but Allow is also default. jobTemplate: spec: template: spec: securityContext: runAsNonRoot: true runAsUser: 1000 seccompProfile: type: RuntimeDefault containers: - name: hello image: busybox args: - /bin/sh - -c - date; echo Hello from the Kubernetes cluster securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL readOnlyRootFilesystem: false restartPolicy: OnFailure ` ================================================ FILE: hack/docs/internal/multiversion-tutorial/webhook_v2_implementaton.go ================================================ /* Copyright 2023 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 multiversion const cronJobFieldsForDefaulting = ` // Default values for various CronJob fields DefaultConcurrencyPolicy batchv2.ConcurrencyPolicy DefaultSuspend bool DefaultSuccessfulJobsHistoryLimit int32 DefaultFailedJobsHistoryLimit int32 ` const cronJobDefaultingLogic = `// Set default values d.applyDefaults(obj) return nil ` const cronJobDefaultFunction = ` // applyDefaults applies default values to CronJob fields. func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv2.CronJob) { if cronJob.Spec.ConcurrencyPolicy == "" { cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy } if cronJob.Spec.Suspend == nil { cronJob.Spec.Suspend = new(bool) *cronJob.Spec.Suspend = d.DefaultSuspend } if cronJob.Spec.SuccessfulJobsHistoryLimit == nil { cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32) *cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit } if cronJob.Spec.FailedJobsHistoryLimit == nil { cronJob.Spec.FailedJobsHistoryLimit = new(int32) *cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit } } ` const cronJobValidationFunction = ` // +kubebuilder:docs-gen:collapse=Webhook Setup and Defaulting // validateCronJob validates the fields of a CronJob object. func validateCronJob(cronjob *batchv2.CronJob) error { var allErrs field.ErrorList if err := validateCronJobName(cronjob); err != nil { allErrs = append(allErrs, err) } if err := validateCronJobSpec(cronjob); err != nil { allErrs = append(allErrs, err) } if len(allErrs) == 0 { return nil } return apierrors.NewInvalid(schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs) } func validateCronJobName(cronjob *batchv2.CronJob) *field.Error { if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 { return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters") } return nil } // validateCronJobSpec validates the schedule format of the custom CronSchedule type func validateCronJobSpec(cronjob *batchv2.CronJob) *field.Error { // Build cron expression from the parts parts := []string{"*", "*", "*", "*", "*"} // default parts for minute, hour, day of month, month, day of week if cronjob.Spec.Schedule.Minute != nil { parts[0] = string(*cronjob.Spec.Schedule.Minute) // Directly cast CronField (which is an alias of string) to string } if cronjob.Spec.Schedule.Hour != nil { parts[1] = string(*cronjob.Spec.Schedule.Hour) } if cronjob.Spec.Schedule.DayOfMonth != nil { parts[2] = string(*cronjob.Spec.Schedule.DayOfMonth) } if cronjob.Spec.Schedule.Month != nil { parts[3] = string(*cronjob.Spec.Schedule.Month) } if cronjob.Spec.Schedule.DayOfWeek != nil { parts[4] = string(*cronjob.Spec.Schedule.DayOfWeek) } // Join parts to form the full cron expression cronExpression := strings.Join(parts, " ") return validateScheduleFormat( cronExpression, field.NewPath("spec").Child("schedule")) } func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error { if _, err := cron.ParseStandard(schedule); err != nil { return field.Invalid(fldPath, schedule, "invalid cron schedule format: "+err.Error()) } return nil } ` const originalSetupManager = `// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr, &batchv2.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{}). Complete() }` const replaceSetupManager = `// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager. func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr, &batchv2.CronJob{}). WithValidator(&CronJobCustomValidator{}). WithDefaulter(&CronJobCustomDefaulter{ DefaultConcurrencyPolicy: batchv2.AllowConcurrent, DefaultSuspend: false, DefaultSuccessfulJobsHistoryLimit: 3, DefaultFailedJobsHistoryLimit: 1, }). Complete() }` ================================================ FILE: hack/docs/internal/utils/utils.go ================================================ /* Copyright 2024 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 utils import ( log "log/slog" "os" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" ) // CheckError will exit with exit code 1 when err is not nil. func CheckError(msg string, err error) { if err != nil { log.Error("error occurred", "message", msg, "error", err) os.Exit(1) } } // NewSampleContext return a context for the Sample func NewSampleContext(binaryPath string, samplePath string, env ...string) utils.TestContext { cmdContext := &utils.CmdContext{ Env: env, Dir: samplePath, } testContext := utils.TestContext{ CmdContext: cmdContext, BinaryName: binaryPath, } return testContext } ================================================ FILE: hack/test/check_go_module.go ================================================ /* Copyright 2025 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. */ // Go module sanity checker validates module compatibility before release: // 1. Validates file paths with x/mod/module.CheckFilePath // 2. Ensures required retracted versions are present in go.mod // 3. Reads module path and Go version from go.mod // 4. Creates a consumer module to test installability // 5. Runs `go mod tidy` and `go build ./...` to verify module works // // This prevents releasing tags that break `go install`. // // Run with: // go run ./hack/test/check_go_module.go package main import ( "bufio" "bytes" "fmt" log "log/slog" "os" "os/exec" "path/filepath" "strings" "golang.org/x/mod/module" ) func main() { if err := checkFilePaths(); err != nil { log.Error("file path validation failed", "error", err) os.Exit(1) } if err := checkRetractedVersions(); err != nil { log.Error("retracted version check failed", "error", err) os.Exit(1) } modulePath, goVersion, err := readGoModInfo() if err != nil { log.Error("failed to read go.mod", "error", err) os.Exit(1) } if err := setupAndCheckConsumer(modulePath, goVersion); err != nil { log.Error("consumer module validation failed", "error", err) os.Exit(1) } log.Info("Go module compatibility check passed") } func checkFilePaths() error { log.Info("Checking Go module file paths") out, err := exec.Command("git", "ls-files").Output() if err != nil { return fmt.Errorf("failed to list git tracked files: %w", err) } var invalidPaths []string for line := range strings.SplitSeq(string(out), "\n") { path := strings.TrimSpace(line) if path == "" { continue } if err := module.CheckFilePath(path); err != nil { invalidPaths = append(invalidPaths, fmt.Sprintf(" %q: %v", path, err)) } } if len(invalidPaths) > 0 { var buf bytes.Buffer buf.WriteString("invalid file paths found:\n") for _, p := range invalidPaths { buf.WriteString(p) buf.WriteByte('\n') } return fmt.Errorf("%s", buf.String()) } log.Info("File path validation passed") return nil } func checkRetractedVersions() error { log.Info("Checking for required retracted versions in go.mod") content, err := os.ReadFile("go.mod") if err != nil { return fmt.Errorf("failed to read go.mod: %w", err) } requiredRetractions := []string{ "retract v4.10.0", // invalid filename causes go get/install failure (#5211) } for _, retract := range requiredRetractions { if !strings.Contains(string(content), retract) { return fmt.Errorf("missing required retraction: %s", retract) } } log.Info("Retracted versions check passed") return nil } func readGoModInfo() (modulePath, goVersion string, err error) { log.Info("Reading module info from go.mod") f, openErr := os.Open("go.mod") if openErr != nil { return "", "", fmt.Errorf("failed to open go.mod: %w", openErr) } defer func() { if closeErr := f.Close(); closeErr != nil { log.Warn("failed to close go.mod", "error", closeErr) } }() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) // Read module path from first line if after, ok := strings.CutPrefix(line, "module "); ok { modulePath = strings.TrimSpace(after) log.Info("Found module path", "module", modulePath) } // Read Go version if after, ok := strings.CutPrefix(line, "go "); ok { goVersion = strings.TrimSpace(after) log.Info("Found Go version", "version", goVersion) } // Stop once we have both if modulePath != "" && goVersion != "" { break } } if modulePath == "" { return "", "", fmt.Errorf("no 'module' directive found in go.mod") } if goVersion == "" { return "", "", fmt.Errorf("no 'go' directive found in go.mod") } return modulePath, goVersion, nil } func setupAndCheckConsumer(modulePath, goVersion string) error { log.Info("Creating consumer module", "module", modulePath, "go_version", goVersion) // Create temporary directory under hack/test/ (covered by **/e2e-*/** in .gitignore) consumerDir := filepath.Join("hack", "test", "e2e-module-check") if err := os.MkdirAll(consumerDir, 0o755); err != nil { return fmt.Errorf("failed to create temp dir: %w", err) } defer func() { if err := os.RemoveAll(consumerDir); err != nil { log.Warn("failed to cleanup temp dir", "dir", consumerDir, "error", err) } }() if err := writeConsumerFiles(consumerDir, modulePath, goVersion); err != nil { return err } log.Info("Running go mod tidy in consumer module") if err := runCommand(consumerDir, "go", "mod", "tidy"); err != nil { return fmt.Errorf("go mod tidy failed: %w", err) } log.Info("Building consumer module") if err := runCommand(consumerDir, "go", "build", "./..."); err != nil { return fmt.Errorf("go build failed: %w", err) } log.Info("Consumer module build succeeded") return nil } func writeConsumerFiles(consumerDir, modulePath, goVersion string) error { goMod := fmt.Sprintf(`module module-consumer go %s require %s v4.0.0-00010101000000-000000000000 replace %s => ../../.. `, goVersion, modulePath, modulePath) // Use a basic import from the module to verify it can be consumed mainGo := fmt.Sprintf(`package main import ( _ "%s/pkg/plugins/golang/v4" ) func main() {} `, modulePath) if err := os.WriteFile(filepath.Join(consumerDir, "go.mod"), []byte(goMod), 0o644); err != nil { return fmt.Errorf("failed to write consumer go.mod: %w", err) } if err := os.WriteFile(filepath.Join(consumerDir, "main.go"), []byte(mainGo), 0o644); err != nil { return fmt.Errorf("failed to write consumer main.go: %w", err) } return nil } // runCommand executes a command in the specified directory with stdout/stderr connected func runCommand(dir, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Dir = dir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("command %s failed in %s: %w", name, dir, err) } return nil } ================================================ FILE: internal/cli/alpha/generate.go ================================================ /* Copyright 2023 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 alpha import ( "log/slog" "os" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal" ) // NewScaffoldCommand returns a new scaffold command, providing the `kubebuilder alpha generate` // feature to re-scaffold projects and assist users with updates. // // IMPORTANT: This command is intended solely for Kubebuilder's use, as it is designed to work // specifically within Kubebuilder's project configuration, key mappings, and plugin initialization. // Its implementation includes fixed values and logic tailored to Kubebuilder’s unique setup, which may // not apply to other projects. Consequently, importing and using this command directly in other projects // will likely result in unexpected behavior, as external projects may have different supported plugin // structures, configurations, and requirements. // // For other projects using Kubebuilder as a library, replicating similar functionality would require // a custom implementation to ensure compatibility with the specific configurations and plugins of that project. // // Technically, implementing functions that allow re-scaffolding with the exact plugins and project-specific // code of external projects is not feasible within Kubebuilder’s current design. func NewScaffoldCommand() *cobra.Command { opts := internal.Generate{} scaffoldCmd := &cobra.Command{ Use: "generate", Short: "Re-scaffold a Kubebuilder project from its PROJECT file", Long: `The 'generate' command re-creates a Kubebuilder project scaffold based on the configuration defined in the PROJECT file, using the latest installed Kubebuilder version and plugins. This is helpful for migrating projects to a newer Kubebuilder layout or plugin version (e.g., v3 to v4) as update your project from any previous version to the current one. If no output directory is provided, the current working directory will be cleaned (except .git and PROJECT).`, Example: ` # **WARNING**(will delete all files to allow the re-scaffold except .git and PROJECT) # Re-scaffold the project in-place kubebuilder alpha generate # Re-scaffold the project from ./test into ./my-output kubebuilder alpha generate --input-dir="./path/to/project" --output-dir="./my-output" `, PreRunE: func(_ *cobra.Command, _ []string) error { return opts.Validate() }, Run: func(_ *cobra.Command, _ []string) { if err := opts.Generate(); err != nil { slog.Error("failed to generate project", "error", err) os.Exit(1) } }, } scaffoldCmd.Flags().StringVar(&opts.InputDir, "input-dir", "", "Path to the directory containing the PROJECT file. "+ "Defaults to the current working directory. WARNING: delete existing files (except .git and PROJECT).") scaffoldCmd.Flags().StringVar(&opts.OutputDir, "output-dir", "", "Directory where the new project scaffold will be written. "+ "If unset, re-scaffolding occurs in-place "+ "and will delete existing files (except .git and PROJECT).") return scaffoldCmd } ================================================ FILE: internal/cli/alpha/generate_test.go ================================================ /* Copyright 2023 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. */ //lint:ignore ST1001 we use dot-imports in tests for brevity package alpha import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("NewScaffoldCommand", func() { When("NewScaffoldCommand", func() { It("Testing the NewScaffoldCommand", func() { cmd := NewScaffoldCommand() Expect(cmd).NotTo(BeNil()) Expect(cmd.Use).NotTo(Equal("")) Expect(cmd.Use).To(ContainSubstring("generate")) Expect(cmd.Short).NotTo(Equal("")) Expect(cmd.Short).To(ContainSubstring("Re-scaffold a Kubebuilder project from its PROJECT file")) Expect(cmd.Example).NotTo(Equal("")) Expect(cmd.Example).To(ContainSubstring("kubebuilder alpha generate")) }) }) }) ================================================ FILE: internal/cli/alpha/internal/common/common.go ================================================ /* Copyright 2025 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 common import ( "fmt" "os" "github.com/spf13/afero" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) // LoadProjectConfig load the project config. func LoadProjectConfig(inputDir string) (store.Store, error) { projectConfig := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()}) if err := projectConfig.LoadFrom(fmt.Sprintf("%s/%s", inputDir, yaml.DefaultPath)); err != nil { return nil, fmt.Errorf("failed to load PROJECT file: %w", err) } return projectConfig, nil } // GetInputPath will return the input path for the project. func GetInputPath(inputPath string) (string, error) { if inputPath == "" { cwd, err := os.Getwd() if err != nil { return "", fmt.Errorf("failed to get working directory: %w", err) } inputPath = cwd } projectPath := fmt.Sprintf("%s/%s", inputPath, yaml.DefaultPath) if _, err := os.Stat(projectPath); os.IsNotExist(err) { return "", fmt.Errorf("project path %q does not exist: %w", projectPath, err) } return inputPath, nil } ================================================ FILE: internal/cli/alpha/internal/common/common_test.go ================================================ //go:build integration /* Copyright 2025 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 common import ( "os" "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" v3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" ) var _ = Describe("LoadProjectConfig", func() { var ( kbc *utils.TestContext projectFile string ) BeforeEach(func() { var err error kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) projectFile = filepath.Join(kbc.Dir, yaml.DefaultPath) }) AfterEach(func() { By("cleaning up test artifacts") kbc.Destroy() }) Context("when PROJECT file exists and is valid", func() { It("should load the project config successfully", func() { config.Register(config.Version{Number: 3}, func() config.Config { return &v3.Cfg{Version: config.Version{Number: 3}} }) const version = `version: "3" ` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) cfg, err := LoadProjectConfig(kbc.Dir) Expect(err).NotTo(HaveOccurred()) Expect(cfg).NotTo(BeNil()) }) }) Context("when PROJECT file does not exist", func() { It("should return an error", func() { _, err := LoadProjectConfig(kbc.Dir) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to load PROJECT file")) }) }) Context("when PROJECT file is invalid", func() { It("should return an error", func() { Expect(os.WriteFile(projectFile, []byte(":?!"), 0o644)).To(Succeed()) _, err := LoadProjectConfig(kbc.Dir) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to load PROJECT file")) }) }) }) var _ = Describe("GetInputPath", func() { var ( kbc *utils.TestContext projectFile string ) BeforeEach(func() { var err error kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) projectFile = filepath.Join(kbc.Dir, yaml.DefaultPath) }) AfterEach(func() { By("cleaning up test artifacts") kbc.Destroy() }) Context("when inputPath has trailing slash", func() { It("should handle trailing slash and find PROJECT file", func() { Expect(os.WriteFile(projectFile, []byte("test"), 0o644)).To(Succeed()) inputPath, err := GetInputPath(kbc.Dir + "/") Expect(err).NotTo(HaveOccurred()) Expect(inputPath).To(Equal(kbc.Dir + "/")) }) }) Context("when inputPath is empty", func() { It("should return error if PROJECT file does not exist in CWD", func() { inputPath, err := GetInputPath("") Expect(err).To(HaveOccurred()) Expect(inputPath).To(Equal("")) Expect(err.Error()).To(ContainSubstring("does not exist")) }) }) Context("when inputPath is valid and PROJECT file exists", func() { It("should return the inputPath", func() { Expect(os.WriteFile(projectFile, []byte("test"), 0o644)).To(Succeed()) inputPath, err := GetInputPath(kbc.Dir) Expect(err).NotTo(HaveOccurred()) Expect(inputPath).To(Equal(kbc.Dir)) }) }) Context("when inputPath is valid but PROJECT file does not exist", func() { It("should return an error", func() { inputPath, err := GetInputPath(kbc.Dir) Expect(err).To(HaveOccurred()) Expect(inputPath).To(Equal("")) Expect(err.Error()).To(ContainSubstring("does not exist")) }) }) Context("when inputPath does not exist", func() { It("should return an error", func() { invalidPath := filepath.Join(kbc.Dir, "nonexistent") inputPath, err := GetInputPath(invalidPath) Expect(err).To(HaveOccurred()) Expect(inputPath).To(Equal("")) Expect(err.Error()).To(ContainSubstring("does not exist")) }) }) }) ================================================ FILE: internal/cli/alpha/internal/common/suite_test.go ================================================ /* Copyright 2025 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 common import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestCommon(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Common Package Suite For Alpha Commands") } ================================================ FILE: internal/cli/alpha/internal/generate.go ================================================ /* Copyright 2023 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 internal import ( "errors" "fmt" "log/slog" "os" "path/filepath" "strings" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/common" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1" autoupdatev1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha" grafanav1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha" helmv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha" helmv2alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha" ) // Generate store the required info for the command type Generate struct { InputDir string OutputDir string } // Define a variable to allow overriding the behavior of getExecutablePath for testing. var getExecutablePathFunc = getExecutablePath // Generate handles the migration and scaffolding process. func (opts *Generate) Generate() error { projectConfig, err := common.LoadProjectConfig(opts.InputDir) if err != nil { return fmt.Errorf("error loading project config: %v", err) } if opts.OutputDir == "" { cwd, getWdErr := os.Getwd() if getWdErr != nil { return fmt.Errorf("failed to get working directory: %w", getWdErr) } opts.OutputDir = cwd if _, err = os.Stat(opts.OutputDir); err == nil { slog.Warn("Using current working directory to re-scaffold the project") slog.Warn("This directory will be cleaned up and all files removed before the re-generation") // Ensure we clean the correct directory slog.Info("Cleaning directory", "dir", opts.OutputDir) // Use an absolute path to target files directly cleanupCmd := fmt.Sprintf("rm -rf %s/*", opts.OutputDir) err = util.RunCmd("Running cleanup", "sh", "-c", cleanupCmd) if err != nil { slog.Error("Cleanup failed", "error", err) return fmt.Errorf("cleanup failed: %w", err) } // Note that we should remove ALL files except the PROJECT file and .git directory cleanupCmd = fmt.Sprintf( `find %q -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {} +`, opts.OutputDir, ) err = util.RunCmd("Running cleanup", "sh", "-c", cleanupCmd) if err != nil { slog.Error("Cleanup failed", "error", err) return fmt.Errorf("cleanup failed: %w", err) } } } if err = createDirectory(opts.OutputDir); err != nil { return fmt.Errorf("error creating output directory %q: %w", opts.OutputDir, err) } if err = changeWorkingDirectory(opts.OutputDir); err != nil { return fmt.Errorf("error changing working directory %q: %w", opts.OutputDir, err) } if err = kubebuilderInit(projectConfig); err != nil { return fmt.Errorf("error initializing project config: %w", err) } if err = kubebuilderCreate(projectConfig); err != nil { return fmt.Errorf("error creating project config: %w", err) } if err = migrateGrafanaPlugin(projectConfig, opts.InputDir, opts.OutputDir); err != nil { return fmt.Errorf("error migrating Grafana plugin: %w", err) } if err = migrateAutoUpdatePlugin(projectConfig); err != nil { return fmt.Errorf("error migrating AutoUpdate plugin: %w", err) } if hasHelm, isV2Alpha := hasHelmPlugin(projectConfig); hasHelm && isV2Alpha { if err = kubebuilderHelmEditWithConfig(projectConfig); err != nil { return fmt.Errorf("error editing Helm plugin: %w", err) } } if err = migrateDeployImagePlugin(projectConfig); err != nil { return fmt.Errorf("error migrating deploy-image plugin: %w", err) } // Run make targets to ensure the project is properly set up. // These steps are performed on a best-effort basis: if any of the targets fail, // we slog a warning to inform the user, but we do not stop the process or return an error. // This is to avoid blocking the migration flow due to non-critical issues during setup. targets := []string{"fmt", "vet", "lint-fix"} for _, target := range targets { err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target) if err != nil { slog.Warn("make target failed", "target", target, "error", err) } } return nil } // Validate ensures the options are valid and kubebuilder is installed. func (opts *Generate) Validate() error { var err error opts.InputDir, err = common.GetInputPath(opts.InputDir) if err != nil { return fmt.Errorf("error getting input path %q: %w", opts.InputDir, err) } _, err = getExecutablePathFunc() if err != nil { return err } return nil } // Helper function to get the PATH of binary. func getExecutablePath() (string, error) { execPath, err := os.Executable() if err != nil { return "", fmt.Errorf("kubebuilder executable not found: %w", err) } realPath, err := filepath.EvalSymlinks(execPath) if err != nil { slog.Warn("Unable to resolve symbolic link", "execPath", execPath, "error", err) // Fallback to execPath return execPath, nil } return realPath, nil } // Helper function to create the output directory. func createDirectory(outputDir string) error { if err := os.MkdirAll(outputDir, 0o755); err != nil { return fmt.Errorf("failed to create output directory %q: %w", outputDir, err) } return nil } // Helper function to change the current working directory. func changeWorkingDirectory(outputDir string) error { if err := os.Chdir(outputDir); err != nil { return fmt.Errorf("failed to change the working directory to %q: %w", outputDir, err) } return nil } // Initializes the project with Kubebuilder. func kubebuilderInit(s store.Store) error { args := append([]string{"init"}, getInitArgs(s)...) execPath, err := getExecutablePathFunc() if err != nil { return err } if err := util.RunCmd("kubebuilder init", execPath, args...); err != nil { return fmt.Errorf("failed to run kubebuilder init command: %w", err) } return nil } // Creates APIs and Webhooks for the project. func kubebuilderCreate(s store.Store) error { resources, err := s.Config().GetResources() if err != nil { return fmt.Errorf("failed to get resources: %w", err) } // First, scaffold all APIs for _, r := range resources { if err = createAPI(r); err != nil { return fmt.Errorf("failed to create API for %s/%s/%s: %w", r.Group, r.Version, r.Kind, err) } } // Then, scaffold all webhooks // We cannot create a webhook for an API that does not exist for _, r := range resources { if err = createWebhook(r); err != nil { return fmt.Errorf("failed to create webhook for %s/%s/%s: %w", r.Group, r.Version, r.Kind, err) } } return nil } // Migrates the Grafana plugin. func migrateGrafanaPlugin(s store.Store, src, des string) error { var grafanaPlugin struct{} key := plugin.GetPluginKeyForConfig(s.Config().GetPluginChain(), grafanav1alpha.Plugin{}) canonicalKey := plugin.KeyFor(grafanav1alpha.Plugin{}) found := true var err error if err = s.Config().DecodePluginConfig(key, grafanaPlugin); err != nil { switch { case errors.As(err, &config.PluginKeyNotFoundError{}): found = false if key != canonicalKey { if err = s.Config().DecodePluginConfig(canonicalKey, grafanaPlugin); err != nil { switch { case errors.As(err, &config.PluginKeyNotFoundError{}): // still not found case errors.As(err, &config.UnsupportedFieldError{}): slog.Info("Project config version does not support plugin metadata, skipping Grafana migration") return nil default: return fmt.Errorf("failed to decode grafana plugin config: %w", err) } } else { found = true } } case errors.As(err, &config.UnsupportedFieldError{}): slog.Info("Project config version does not support plugin metadata, skipping Grafana migration") return nil default: return fmt.Errorf("failed to decode grafana plugin config: %w", err) } } if !found { slog.Info("Grafana plugin not found, skipping migration") return nil } if err = kubebuilderGrafanaEdit(); err != nil { return fmt.Errorf("error editing Grafana plugin: %w", err) } if err = grafanaConfigMigrate(src, des); err != nil { return fmt.Errorf("error migrating Grafana config: %w", err) } return kubebuilderGrafanaEdit() } func migrateAutoUpdatePlugin(s store.Store) error { key := plugin.GetPluginKeyForConfig(s.Config().GetPluginChain(), autoupdatev1alpha.Plugin{}) canonicalKey := plugin.KeyFor(autoupdatev1alpha.Plugin{}) var autoUpdatePlugin autoupdatev1alpha.PluginConfig found := true var err error err = s.Config().DecodePluginConfig(key, &autoUpdatePlugin) if err != nil { switch { case errors.As(err, &config.PluginKeyNotFoundError{}): found = false if key != canonicalKey { if err = s.Config().DecodePluginConfig(canonicalKey, &autoUpdatePlugin); err != nil { switch { case errors.As(err, &config.PluginKeyNotFoundError{}): // still not found case errors.As(err, &config.UnsupportedFieldError{}): slog.Info("Project config version does not support plugin metadata, skipping Auto Update migration") return nil default: return fmt.Errorf("failed to decode autoupdate plugin config: %w", err) } } else { found = true } } case errors.As(err, &config.UnsupportedFieldError{}): slog.Info("Project config version does not support plugin metadata, skipping Auto Update migration") return nil default: return fmt.Errorf("failed to decode autoupdate plugin config: %w", err) } } if !found { slog.Info("Auto Update plugin not found, skipping migration") return nil } args := []string{"edit", "--plugins", plugin.KeyFor(autoupdatev1alpha.Plugin{})} if autoUpdatePlugin.UseGHModels { args = append(args, "--use-gh-models") } if err = util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil { return fmt.Errorf("failed to run edit subcommand for Auto plugin: %w", err) } return nil } // Migrates the Deploy Image plugin. func migrateDeployImagePlugin(s store.Store) error { key := plugin.GetPluginKeyForConfig(s.Config().GetPluginChain(), deployimagev1alpha1.Plugin{}) canonicalKey := plugin.KeyFor(deployimagev1alpha1.Plugin{}) var deployImagePlugin deployimagev1alpha1.PluginConfig found := true var err error err = s.Config().DecodePluginConfig(key, &deployImagePlugin) if err != nil { switch { case errors.As(err, &config.PluginKeyNotFoundError{}): found = false if key != canonicalKey { if err = s.Config().DecodePluginConfig(canonicalKey, &deployImagePlugin); err != nil { switch { case errors.As(err, &config.PluginKeyNotFoundError{}): // still not found case errors.As(err, &config.UnsupportedFieldError{}): slog.Info("Project config version does not support plugin metadata, skipping Deploy Image migration") return nil default: return fmt.Errorf("failed to decode deploy-image plugin config: %w", err) } } else { found = true } } case errors.As(err, &config.UnsupportedFieldError{}): slog.Info("Project config version does not support plugin metadata, skipping Deploy Image migration") return nil default: return fmt.Errorf("failed to decode deploy-image plugin config: %w", err) } } if !found { slog.Info("Deploy-image plugin not found, skipping migration") return nil } for _, r := range deployImagePlugin.Resources { if err := createAPIWithDeployImage(r); err != nil { return fmt.Errorf("failed to create API with deploy-image: %w", err) } } return nil } // Creates an API with Deploy Image plugin. func createAPIWithDeployImage(resourceData deployimagev1alpha1.ResourceData) error { args := append([]string{"create", "api"}, getGVKFlagsFromDeployImage(resourceData)...) args = append(args, getDeployImageOptions(resourceData)...) if err := util.RunCmd("kubebuilder create api", "kubebuilder", args...); err != nil { return fmt.Errorf("failed to run kubebuilder create api command: %w", err) } return nil } // Helper function to get Init arguments for Kubebuilder. func getInitArgs(s store.Store) []string { var args []string plugins := s.Config().GetPluginChain() // Define outdated plugin versions that need replacement outdatedPlugins := map[string]string{ "go.kubebuilder.io/v3": "go.kubebuilder.io/v4", "go.kubebuilder.io/v3-alpha": "go.kubebuilder.io/v4", "go.kubebuilder.io/v2": "go.kubebuilder.io/v4", "helm.kubebuilder.io/v1-alpha": "helm.kubebuilder.io/v2-alpha", } // Replace outdated plugins and exit after the first replacement for i, plg := range plugins { if newPlugin, exists := outdatedPlugins[plg]; exists { slog.Warn("We checked that your PROJECT file is configured with deprecated layout. "+ "However, we will try our best to re-generate the project using new one", "deprecated_layout", plg, "new_layout", newPlugin) plugins[i] = newPlugin break } } if len(plugins) > 0 { args = append(args, "--plugins", strings.Join(plugins, ",")) } if domain := s.Config().GetDomain(); domain != "" { args = append(args, "--domain", domain) } if repo := s.Config().GetRepository(); repo != "" { args = append(args, "--repo", repo) } if projectName := s.Config().GetProjectName(); projectName != "" { args = append(args, "--project-name", projectName) } if s.Config().IsMultiGroup() { args = append(args, "--multigroup") } if s.Config().IsNamespaced() { args = append(args, "--namespaced") } return args } // Gets the GVK flags for a resource. func getGVKFlags(res resource.Resource) []string { var args []string if res.Plural != "" { args = append(args, "--plural", res.Plural) } if res.Group != "" { args = append(args, "--group", res.Group) } if res.Version != "" { args = append(args, "--version", res.Version) } if res.Kind != "" { args = append(args, "--kind", res.Kind) } return args } // Gets the GVK flags for a Deploy Image resource. func getGVKFlagsFromDeployImage(resourceData deployimagev1alpha1.ResourceData) []string { var args []string if resourceData.Group != "" { args = append(args, "--group", resourceData.Group) } if resourceData.Version != "" { args = append(args, "--version", resourceData.Version) } if resourceData.Kind != "" { args = append(args, "--kind", resourceData.Kind) } return args } // Gets the options for a Deploy Image resource. func getDeployImageOptions(resourceData deployimagev1alpha1.ResourceData) []string { var args []string if resourceData.Options.Image != "" { args = append(args, fmt.Sprintf("--image=%s", resourceData.Options.Image)) } if resourceData.Options.ContainerCommand != "" { args = append(args, fmt.Sprintf("--image-container-command=%s", resourceData.Options.ContainerCommand)) } if resourceData.Options.ContainerPort != "" { args = append(args, fmt.Sprintf("--image-container-port=%s", resourceData.Options.ContainerPort)) } if resourceData.Options.RunAsUser != "" { args = append(args, fmt.Sprintf("--run-as-user=%s", resourceData.Options.RunAsUser)) } args = append(args, fmt.Sprintf("--plugins=%s", plugin.KeyFor(deployimagev1alpha1.Plugin{}))) return args } // Creates an API resource. func createAPI(res resource.Resource) error { args := append([]string{"create", "api"}, getGVKFlags(res)...) args = append(args, getAPIResourceFlags(res)...) // Add the external API flags if the resource is external if res.IsExternal() { args = append(args, "--external-api-path", res.Path) args = append(args, "--external-api-domain", res.Domain) // Add module if specified if res.Module != "" { args = append(args, "--external-api-module", res.Module) } } if err := util.RunCmd("kubebuilder create api", "kubebuilder", args...); err != nil { return fmt.Errorf("failed to run kubebuilder create api command: %w", err) } return nil } // Gets flags for API resource creation. func getAPIResourceFlags(res resource.Resource) []string { var args []string if res.API == nil || res.API.IsEmpty() { args = append(args, "--resource=false") } else { args = append(args, "--resource") if res.API.Namespaced { args = append(args, "--namespaced") } else { args = append(args, "--namespaced=false") } } if res.Controller { args = append(args, "--controller") } else { args = append(args, "--controller=false") } return args } // Creates a webhook resource. func createWebhook(res resource.Resource) error { if res.Webhooks == nil || res.Webhooks.IsEmpty() { return nil } args := append([]string{"create", "webhook"}, getGVKFlags(res)...) args = append(args, getWebhookResourceFlags(res)...) if err := util.RunCmd("kubebuilder create webhook", "kubebuilder", args...); err != nil { return fmt.Errorf("failed to run kubebuilder create webhook command: %w", err) } return nil } // Gets flags for webhook creation. func getWebhookResourceFlags(res resource.Resource) []string { var args []string if res.IsExternal() { args = append(args, "--external-api-path", res.Path) args = append(args, "--external-api-domain", res.Domain) // Add module if specified if res.Module != "" { args = append(args, "--external-api-module", res.Module) } } if res.HasValidationWebhook() { args = append(args, "--programmatic-validation") if res.Webhooks.ValidationPath != "" { args = append(args, "--validation-path", res.Webhooks.ValidationPath) } } if res.HasDefaultingWebhook() { args = append(args, "--defaulting") if res.Webhooks.DefaultingPath != "" { args = append(args, "--defaulting-path", res.Webhooks.DefaultingPath) } } if res.HasConversionWebhook() { args = append(args, "--conversion") if len(res.Webhooks.Spoke) > 0 { for _, spoke := range res.Webhooks.Spoke { args = append(args, "--spoke", spoke) } } // Note: conversion webhooks don't use custom path flags } return args } // Copies files from source to destination. func copyFile(src, des string) error { bytesRead, err := os.ReadFile(src) if err != nil { return fmt.Errorf("source file path %q does not exist: %w", src, err) } if err = os.WriteFile(des, bytesRead, 0o755); err != nil { return fmt.Errorf("failed to write file %q: %w", des, err) } return nil } // Migrates Grafana configuration files. func grafanaConfigMigrate(src, des string) error { grafanaConfig := fmt.Sprintf("%s/grafana/custom-metrics/config.yaml", src) if _, err := os.Stat(grafanaConfig); os.IsNotExist(err) { slog.Info("Grafana config file not found, skipping file migration", "path", grafanaConfig) return nil // Don't fail if config files don't exist } return copyFile(grafanaConfig, fmt.Sprintf("%s/grafana/custom-metrics/config.yaml", des)) } // Edits the project to include the Grafana plugin. func kubebuilderGrafanaEdit() error { args := []string{"edit", "--plugins", plugin.KeyFor(grafanav1alpha.Plugin{})} if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil { return fmt.Errorf("failed to run edit subcommand for Grafana plugin: %w", err) } return nil } // Edits the project to include the Helm plugin with tracked configuration. func kubebuilderHelmEditWithConfig(s store.Store) error { var cfg struct { ManifestsFile string `json:"manifests,omitempty"` OutputDir string `json:"output,omitempty"` } err := s.Config().DecodePluginConfig(plugin.KeyFor(helmv2alpha.Plugin{}), &cfg) if errors.As(err, &config.PluginKeyNotFoundError{}) { // No previous configuration, use defaults return kubebuilderHelmEdit(true) } else if err != nil { return fmt.Errorf("failed to decode helm plugin config: %w", err) } // Use tracked configuration values pluginKey := plugin.KeyFor(helmv2alpha.Plugin{}) args := []string{"edit", "--plugins", pluginKey} if cfg.ManifestsFile != "" { args = append(args, "--manifests", cfg.ManifestsFile) } if cfg.OutputDir != "" { args = append(args, "--output-dir", cfg.OutputDir) } if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil { return fmt.Errorf("failed to run edit subcommand for Helm plugin: %w", err) } return nil } // Edits the project to include the Helm plugin. func kubebuilderHelmEdit(isV2Alpha bool) error { var pluginKey string if isV2Alpha { pluginKey = plugin.KeyFor(helmv2alpha.Plugin{}) } else { pluginKey = plugin.KeyFor(helmv1alpha.Plugin{}) } args := []string{"edit", "--plugins", pluginKey} if err := util.RunCmd("kubebuilder edit", "kubebuilder", args...); err != nil { return fmt.Errorf("failed to run edit subcommand for Helm plugin: %w", err) } return nil } // hasHelmPlugin checks if any Helm plugin (v1alpha or v2alpha) is present by inspecting // the plugin chain or configuration. func hasHelmPlugin(cfg store.Store) (bool, bool) { var pluginConfig map[string]any // Check for v2alpha first (preferred) err := cfg.Config().DecodePluginConfig(plugin.KeyFor(helmv2alpha.Plugin{}), &pluginConfig) if err == nil { return true, true // has helm plugin, is v2alpha } // Check for v1alpha err = cfg.Config().DecodePluginConfig(plugin.KeyFor(helmv1alpha.Plugin{}), &pluginConfig) if err != nil { // If neither Helm plugin is found, return false if errors.As(err, &config.PluginKeyNotFoundError{}) { return false, false } // slog other errors if needed slog.Error("error decoding Helm plugin config", "error", err) return false, false } // v1alpha Helm plugin is present return true, false // has helm plugin, is not v2alpha } ================================================ FILE: internal/cli/alpha/internal/generate_test.go ================================================ /* Copyright 2025 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 internal import ( "fmt" "os" "path/filepath" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1" autoupdatev1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" ) type fakeConfig struct { config.Config pluginChain []string domain string repo string projectName string multigroup bool namespaced bool resources []resource.Resource pluginErr error getResErr error plugins map[string]any } func (f *fakeConfig) GetPluginChain() []string { return f.pluginChain } func (f *fakeConfig) GetDomain() string { return f.domain } func (f *fakeConfig) GetRepository() string { return f.repo } func (f *fakeConfig) GetProjectName() string { return f.projectName } func (f *fakeConfig) IsMultiGroup() bool { return f.multigroup } func (f *fakeConfig) IsNamespaced() bool { return f.namespaced } func (f *fakeConfig) GetResources() ([]resource.Resource, error) { if f.getResErr != nil { return nil, f.getResErr } return f.resources, nil } func (f *fakeConfig) DecodePluginConfig(key string, dst any) error { if len(f.plugins) == 0 { return config.PluginKeyNotFoundError{Key: key} } if f.pluginErr != nil { return f.pluginErr } // Check if the specific key exists val, exists := f.plugins[key] if !exists { return config.PluginKeyNotFoundError{Key: key} } // If the value is a struct, copy its fields to dst if val != nil && dst != nil { // Handle different plugin config types switch d := dst.(type) { case *autoupdatev1alpha.PluginConfig: if v, ok := val.(autoupdatev1alpha.PluginConfig); ok { *d = v } case *deployimagev1alpha1.PluginConfig: if v, ok := val.(deployimagev1alpha1.PluginConfig); ok { *d = v } } } return nil } type fakeStore struct { store.Store cfg *fakeConfig } func (f *fakeStore) Config() config.Config { return f.cfg } func TestGenerateHelpers(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Generate helpers Suite") } // setupKubebuilderMockEnvironment sets up a mock for kubebuilder testing func setupKubebuilderMockEnvironment(kbc *utils.TestContext) string { // Save current working directory for restoration originalDir, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) // Create a PROJECT file in the test directory with proper YAML format projectFilePath := filepath.Join(kbc.Dir, "PROJECT") projectFileContent := []byte(`# Code generated by tool. DO NOT EDIT. # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html domain: example.com layout: - go.kubebuilder.io/v4 projectName: test-project repo: github.com/example/test-project version: "3" `) Expect(os.WriteFile(projectFilePath, projectFileContent, 0o644)).To(Succeed()) // Create mock kubebuilder binary mockKubebuilderPath := filepath.Join(kbc.Dir, "kubebuilder") mockScript := `#!/bin/bash # Log all commands to a file for debugging log_file="` + filepath.Join(kbc.Dir, "kubebuilder.log") + `" echo "$@" >> "$log_file" if [[ "$1" == "init" ]]; then echo "kubebuilder init mock executed" exit 0 elif [[ "$1" == "create" && "$2" == "api" ]]; then echo "kubebuilder create api mock executed" exit 0 elif [[ "$1" == "create" && "$2" == "webhook" ]]; then echo "kubebuilder create webhook mock executed" exit 0 elif [[ "$1" == "edit" ]]; then echo "kubebuilder edit mock executed" exit 0 else echo "kubebuilder mock executed with args: $@" exit 0 fi` Expect(os.WriteFile(mockKubebuilderPath, []byte(mockScript), 0o755)).To(Succeed()) // Create mock make binary mockMakePath := filepath.Join(kbc.Dir, "make") makeScript := `#!/bin/bash # Log all commands to a file for debugging log_file="` + filepath.Join(kbc.Dir, "make.log") + `" echo "$@" >> "$log_file" echo "make mock executed with target: $@" exit 0` Expect(os.WriteFile(mockMakePath, []byte(makeScript), 0o755)).To(Succeed()) // Add the test directory to PATH so the mock binaries are found oldPath := os.Getenv("PATH") newPath := fmt.Sprintf("%s:%s", kbc.Dir, oldPath) Expect(os.Setenv("PATH", newPath)).To(Succeed()) // Change to the test directory so kubebuilder can find the PROJECT file Expect(os.Chdir(kbc.Dir)).To(Succeed()) // Return the original directory for restoration return originalDir } var _ = Describe("generate: validate", func() { var ( kbc *utils.TestContext err error ) BeforeEach(func() { // Initialize TestContext kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) // Create a PROJECT file in the test directory projectFilePath := filepath.Join(kbc.Dir, "PROJECT") projectFileContent := []byte("domain: example.com\nrepo: github.com/example/repo\n") Expect(os.WriteFile(projectFilePath, projectFileContent, 0o644)).To(Succeed()) }) AfterEach(func() { By("cleaning up test artifacts") kbc.Destroy() }) // Validate Context("Validate", func() { Context("Success", func() { It("succeeds", func() { g := &Generate{InputDir: kbc.Dir} Expect(g.Validate()).To(Succeed()) }) }) Context("Failure", func() { It("returns error if GetInputPath fails", func() { g := &Generate{InputDir: filepath.Join(kbc.Dir, "notfound")} Expect(g.Validate()).NotTo(Succeed()) }) }) }) }) var _ = Describe("generate: directory-helpers", func() { var ( tmpDir string err error ) BeforeEach(func() { tmpDir, err = os.MkdirTemp("", "testdir") Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { Expect(os.RemoveAll(tmpDir)).To(Succeed()) }) // createDirectory Context("createDirectory", func() { It("creates directory successfully", func() { dir := filepath.Join(tmpDir, "testdir-generate-go") Expect(createDirectory(dir)).To(Succeed()) _, err = os.Stat(dir) Expect(err).NotTo(HaveOccurred()) }) It("returns error for invalid path", func() { Expect(createDirectory("/dev/null/foo")).NotTo(Succeed()) }) }) // changeWorkingDirectory Context("changeWorkingDirectory", func() { var originalDir string BeforeEach(func() { // Save current working directory originalDir, err = os.Getwd() Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { // Restore original working directory Expect(os.Chdir(originalDir)).To(Succeed()) }) It("changes working directory successfully", func() { // Create a test file in the target directory to verify we can access it after changing testFile := filepath.Join(tmpDir, "test-file.txt") testContent := "test content" Expect(os.WriteFile(testFile, []byte(testContent), 0o644)).To(Succeed()) // Change to the directory Expect(changeWorkingDirectory(tmpDir)).To(Succeed()) // Verify we're in the correct directory by checking we can read the file // using relative path (this proves we're in the right directory) content, err := os.ReadFile("test-file.txt") Expect(err).NotTo(HaveOccurred()) Expect(string(content)).To(Equal(testContent)) // Also verify we can create a new file in the current directory newFile := "new-test-file.txt" newContent := "new content" Expect(os.WriteFile(newFile, []byte(newContent), 0o644)).To(Succeed()) // Verify the file was created in the expected location fullPath := filepath.Join(tmpDir, newFile) verifyContent, err := os.ReadFile(fullPath) Expect(err).NotTo(HaveOccurred()) Expect(string(verifyContent)).To(Equal(newContent)) }) It("returns error for non-existent directory", func() { nonExistentDir := filepath.Join(tmpDir, "nonexistent") Expect(changeWorkingDirectory(nonExistentDir)).NotTo(Succeed()) }) It("returns error for invalid path", func() { Expect(changeWorkingDirectory("/dev/null/foo")).NotTo(Succeed()) }) }) }) var _ = Describe("generate: file-helpers", func() { var ( tmpDir string err error ) BeforeEach(func() { tmpDir, err = os.MkdirTemp("", "testdir") Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { Expect(os.RemoveAll(tmpDir)).To(Succeed()) }) // copyFile Context("copyFile", func() { Context("success", func() { var src, dst string BeforeEach(func() { src = filepath.Join(tmpDir, "src.txt") dst = filepath.Join(tmpDir, "dst.txt") Expect(os.WriteFile(src, []byte("hello"), 0o644)).To(Succeed()) }) AfterEach(func() { Expect(os.Remove(src)).To(Succeed()) Expect(os.Remove(dst)).To(Succeed()) }) It("copies file successfully", func() { Expect(copyFile(src, dst)).To(Succeed()) b, err := os.ReadFile(dst) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal("hello")) }) }) Context("failure", func() { It("returns error if src does not exist", func() { src := filepath.Join(tmpDir, "notfound") dst := filepath.Join(tmpDir, "nowhere") Expect(copyFile(src, dst)).NotTo(Succeed()) }) }) }) }) var _ = Describe("generate: get-args-helpers", func() { // getInitArgs Describe("getInitArgs", func() { Context("for outdated plugins", func() { When("v3 plugin is used", func() { It("should return correct args for plugins, domain, repo", func() { cfg := &fakeConfig{pluginChain: []string{"go.kubebuilder.io/v3"}, domain: "foo.com", repo: "bar"} store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"), "--domain", "foo.com", "--repo", "bar")) }) }) When("alpha plugin is used", func() { It("should return correct args for plugins, domain, repo", func() { cfg := &fakeConfig{pluginChain: []string{"go.kubebuilder.io/v3-alpha"}, domain: "foo.com", repo: "bar"} store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"), "--domain", "foo.com", "--repo", "bar")) }) }) When("helm v1-alpha plugin is used", func() { It("should replace with helm v2-alpha", func() { cfg := &fakeConfig{ pluginChain: []string{"go.kubebuilder.io/v4", "helm.kubebuilder.io/v1-alpha"}, domain: "foo.com", repo: "bar", } store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("helm.kubebuilder.io/v2-alpha"), "--domain", "foo.com", "--repo", "bar")) Expect(args).NotTo(ContainElement(ContainSubstring("helm.kubebuilder.io/v1-alpha"))) }) }) }) Context("for latest plugins", func() { When("latest plugin (v4) is used", func() { It("returns correct args for plugins, domain, repo", func() { cfg := &fakeConfig{pluginChain: []string{"go.kubebuilder.io/v4"}, domain: "foo.com", repo: "bar"} store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"), "--domain", "foo.com", "--repo", "bar")) }) }) When("project name is set", func() { It("returns correct args including project name", func() { cfg := &fakeConfig{ pluginChain: []string{"go.kubebuilder.io/v4"}, domain: "foo.com", repo: "bar", projectName: "my-project", } store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"), "--domain", "foo.com", "--repo", "bar", "--project-name", "my-project")) }) }) When("multigroup flag is enabled", func() { It("includes --multigroup in init args", func() { cfg := &fakeConfig{ pluginChain: []string{"go.kubebuilder.io/v4"}, domain: "foo.com", repo: "bar", multigroup: true, } store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"), "--domain", "foo.com", "--repo", "bar", "--multigroup")) }) }) When("namespaced flag is enabled", func() { It("includes --namespaced in init args", func() { cfg := &fakeConfig{ pluginChain: []string{"go.kubebuilder.io/v4"}, domain: "foo.com", repo: "bar", namespaced: true, } store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"), "--domain", "foo.com", "--repo", "bar", "--namespaced")) }) }) When("both multigroup and namespaced are enabled", func() { It("includes both flags in init args", func() { cfg := &fakeConfig{ pluginChain: []string{"go.kubebuilder.io/v4"}, domain: "foo.com", repo: "bar", multigroup: true, namespaced: true, } store := &fakeStore{cfg: cfg} args := getInitArgs(store) Expect(args).To(ContainElements("--plugins", ContainSubstring("go.kubebuilder.io/v4"), "--domain", "foo.com", "--repo", "bar", "--multigroup", "--namespaced")) }) }) }) }) // getGVKFlags Context("getGVKFlags", func() { It("returns correct flags", func() { res := resource.Resource{Plural: "foos"} res.Group = "example.com" res.Version = "v1" res.Kind = "Foo" flags := getGVKFlags(res) Expect(flags).To(ContainElements("--plural", "foos", "--group", "example.com", "--version", "v1", "--kind", "Foo")) }) }) // getGVKFlagsFromDeployImage Context("getGVKFlagsFromDeployImage", func() { It("returns correct flags", func() { rd := deployimagev1alpha1.ResourceData{Group: "example.com", Version: "v1", Kind: "Foo"} flags := getGVKFlagsFromDeployImage(rd) Expect(flags).To(ContainElements("--group", "example.com", "--version", "v1", "--kind", "Foo")) }) }) // getDeployImageOptions Context("getDeployImageOptions", func() { It("returns correct options", func() { rd := deployimagev1alpha1.ResourceData{} rd.Options.Image = "test-kubebuilder" rd.Options.ContainerCommand = "echo 'Hello'" rd.Options.ContainerPort = "8000" rd.Options.RunAsUser = "test" opts := getDeployImageOptions(rd) Expect(opts).To(ContainElements("--image=test-kubebuilder", "--image-container-command=echo 'Hello'", "--image-container-port=8000", "--run-as-user=test", "--plugins=deploy-image.go.kubebuilder.io/v1-alpha")) }) }) // getAPIResourceFlags Context("getAPIResourceFlags", func() { var res resource.Resource BeforeEach(func() { res = resource.Resource{API: &resource.API{}} }) Context("returns correct flags", func() { It("for nil API with Controller set", func() { res.Controller = true Expect(getAPIResourceFlags(res)).To(ContainElements("--resource=false", "--controller")) }) It("for non nil API (namespaced not set) with Controller not set", func() { res.API.CRDVersion = "v1" res.API.Namespaced = true Expect(getAPIResourceFlags(res)).To(ContainElements("--resource", "--namespaced", "--controller=false")) }) It("for non nil API (namespaced set) with Controller not set", func() { res.API.CRDVersion = "v1" res.API.Namespaced = false Expect(getAPIResourceFlags(res)).To(ContainElements("--resource", "--namespaced=false", "--controller=false")) }) }) }) // getWebhookResourceFlags Context("getWebhookResourceFlags", func() { It("returns correct flags for specified resources", func() { res := resource.Resource{ Path: "external/test", GVK: resource.GVK{Group: "example.com", Version: "v1", Kind: "Example", Domain: "test"}, External: true, Webhooks: &resource.Webhooks{ Validation: true, Defaulting: true, Conversion: true, Spoke: []string{"v2"}, }, } flags := getWebhookResourceFlags(res) Expect(flags).To(ContainElements("--external-api-path", "external/test", "--external-api-domain", "test", "--programmatic-validation", "--defaulting", "--conversion", "--spoke", "v2")) }) It("returns correct flags for external resources with module version", func() { res := resource.Resource{ Path: "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1", Module: "github.com/cert-manager/cert-manager@v1.18.2", GVK: resource.GVK{Group: "cert-manager", Version: "v1", Kind: "Certificate", Domain: "io"}, External: true, Webhooks: &resource.Webhooks{ Defaulting: true, }, } flags := getWebhookResourceFlags(res) Expect(flags).To(ContainElement("--external-api-path")) Expect(flags).To(ContainElement("github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1")) Expect(flags).To(ContainElement("--external-api-domain")) Expect(flags).To(ContainElement("io")) Expect(flags).To(ContainElement("--external-api-module")) Expect(flags).To(ContainElement("github.com/cert-manager/cert-manager@v1.18.2")) Expect(flags).To(ContainElement("--defaulting")) }) It("returns correct flags for external resources WITHOUT module version", func() { res := resource.Resource{ Path: "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1", Module: "", // No module specified GVK: resource.GVK{Group: "cert-manager", Version: "v1", Kind: "Certificate", Domain: "io"}, External: true, Webhooks: &resource.Webhooks{ Defaulting: true, }, } flags := getWebhookResourceFlags(res) Expect(flags).To(ContainElement("--external-api-path")) Expect(flags).To(ContainElement("github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1")) Expect(flags).To(ContainElement("--external-api-domain")) Expect(flags).To(ContainElement("io")) Expect(flags).NotTo(ContainElement("--external-api-module")) Expect(flags).To(ContainElement("--defaulting")) }) }) }) var _ = Describe("generate: create-helpers", func() { var ( kbc *utils.TestContext err error originalDir string ) BeforeEach(func() { // Initialize TestContext kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) // Setup mock kubebuilder environment originalDir = setupKubebuilderMockEnvironment(kbc) }) AfterEach(func() { By("cleaning up test artifacts") // Restore original working directory Expect(os.Chdir(originalDir)).To(Succeed()) kbc.Destroy() }) // createAPI Describe("createAPI", func() { Context("Without External flag", func() { It("runs kubebuilder create api successfully for a resource", func() { res := resource.Resource{ GVK: resource.GVK{Group: "example.com", Version: "v1", Kind: "Example", Domain: "test"}, Plural: "examples", API: &resource.API{Namespaced: true}, Controller: true, } // Run createAPI and verify no errors Expect(createAPI(res)).To(Succeed()) }) }) Context("With External flag set", func() { It("runs kubebuilder create api successfully for a resource", func() { res := resource.Resource{ GVK: resource.GVK{Group: "example.com", Version: "v1", Kind: "Example", Domain: "external"}, Plural: "examples", API: &resource.API{Namespaced: true}, Controller: true, External: true, Path: "external/path", } // Run createAPI and verify no errors Expect(createAPI(res)).To(Succeed()) }) It("runs kubebuilder create api successfully with module version", func() { res := resource.Resource{ GVK: resource.GVK{Group: "cert-manager", Version: "v1", Kind: "Certificate", Domain: "io"}, Plural: "certificates", API: nil, // External resources typically don't scaffold API Controller: true, External: true, Path: "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1", Module: "github.com/cert-manager/cert-manager@v1.18.2", } // Run createAPI and verify no errors Expect(createAPI(res)).To(Succeed()) }) It("runs kubebuilder create api successfully WITHOUT module version", func() { res := resource.Resource{ GVK: resource.GVK{Group: "cert-manager", Version: "v1", Kind: "Certificate", Domain: "io"}, Plural: "certificates", API: nil, Controller: true, External: true, Path: "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1", Module: "", // No module specified } // Run createAPI and verify no errors Expect(createAPI(res)).To(Succeed()) }) }) }) // createWebhook Describe("createWebhook", func() { It("runs kubebuilder create webhook successfully for a resource", func() { res := resource.Resource{ GVK: resource.GVK{Group: "example.com", Version: "v1", Kind: "Example", Domain: "test"}, Plural: "examples", Webhooks: &resource.Webhooks{WebhookVersion: "v1"}, } // Run createWebhook and verify no errors Expect(createWebhook(res)).To(Succeed()) }) It("ignores web creation if webhook resource is empty", func() { res := resource.Resource{ GVK: resource.GVK{Group: "example.com", Version: "v1", Kind: "Example", Domain: "test"}, Plural: "examples", Webhooks: &resource.Webhooks{}, } // Run createWebhook and verify no errors Expect(createWebhook(res)).To(Succeed()) }) }) Describe("createAPIWithDeployImage", func() { It("runs kubebuilder create api successfully with deploy image", func() { resourceData := deployimagev1alpha1.ResourceData{ Group: "example.com", Version: "v1", Kind: "Example", } resourceData.Options.Image = "example-image" resourceData.Options.ContainerCommand = "run" resourceData.Options.ContainerPort = "8080" resourceData.Options.Image = "test" // Run createAPIWithDeployImage and verify no errors Expect(createAPIWithDeployImage(resourceData)).To(Succeed()) }) It("validates deploy-image works with external APIs without release version", func() { // This test validates that deploy-image plugin can work with external APIs // even without pinned versions (backward compatibility) resourceData := deployimagev1alpha1.ResourceData{ Group: "cert-manager", Domain: "io", Version: "v1", Kind: "Certificate", } resourceData.Options.Image = "busybox:1.36.1" resourceData.Options.RunAsUser = "1001" // Run createAPIWithDeployImage and verify no errors Expect(createAPIWithDeployImage(resourceData)).To(Succeed()) }) It("validates deploy-image can be used alongside external APIs with release version", func() { // This test validates that when external APIs with release versions are used, // deploy-image plugin still works correctly // Note: The release field is stored in the Resource, not in DeployImage's ResourceData resourceData := deployimagev1alpha1.ResourceData{ Group: "example.com", Version: "v1", Kind: "Memcached", } resourceData.Options.Image = "memcached:1.6.26" resourceData.Options.ContainerPort = "11211" // Run createAPIWithDeployImage and verify no errors Expect(createAPIWithDeployImage(resourceData)).To(Succeed()) }) }) }) var _ = Describe("generate: kubebuilder", func() { var ( kbc *utils.TestContext err error originalDir string originalGetExecutablePathFunc func() (string, error) ) BeforeEach(func() { // Save the original function originalGetExecutablePathFunc = getExecutablePathFunc // Initialize TestContext kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) // Setup mock kubebuilder environment originalDir = setupKubebuilderMockEnvironment(kbc) // Mock getExecutablePathFunc to return the mock kubebuilder binary path getExecutablePathFunc = func() (string, error) { return filepath.Join(kbc.Dir, "kubebuilder"), nil } }) AfterEach(func() { // Restore the original getExecutablePath function getExecutablePathFunc = originalGetExecutablePathFunc // Restore original working directory Expect(os.Chdir(originalDir)).To(Succeed()) // Clean up test artifacts kbc.Destroy() }) Context("kubebuilderInit", func() { It("runs kubebuilder init successfully", func() { cfg := &fakeConfig{ pluginChain: []string{"go.kubebuilder.io/v4"}, domain: "example.com", repo: "github.com/example/repo", } store := &fakeStore{cfg: cfg} Expect(kubebuilderInit(store)).To(Succeed()) }) }) Context("kubebuilderCreate", func() { It("runs kubebuilder create successfully for resources", func() { cfg := &fakeConfig{ resources: []resource.Resource{ {Plural: "foos", GVK: resource.GVK{Group: "example.com", Version: "v1", Kind: "Foo"}}, {Plural: "bars", GVK: resource.GVK{Group: "example.com", Version: "v1", Kind: "Bar"}}, }, } store := &fakeStore{cfg: cfg} // Run kubebuilderCreate and verify no errors Expect(kubebuilderCreate(store)).To(Succeed()) }) }) Context("kubebuilderGrafanaEdit", func() { It("runs kubebuilder edit successfully for Grafana plugin", func() { // Run kubebuilderGrafanaEdit and verify no errors Expect(kubebuilderGrafanaEdit()).To(Succeed()) }) }) Context("kubebuilderHelmEdit", func() { It("runs kubebuilder edit successfully for Helm plugin", func() { // Run kubebuilderHelmEdit and verify no errors Expect(kubebuilderHelmEdit(true)).To(Succeed()) }) }) }) var _ = Describe("generate: hasHelmPlugin", func() { It("returns true if v2-alpha plugin present", func() { cfg := &fakeConfig{plugins: map[string]any{"helm.kubebuilder.io/v2-alpha": true}} store := &fakeStore{cfg: cfg} hasPlugin, isV2Alpha := hasHelmPlugin(store) Expect(hasPlugin).To(BeTrue()) Expect(isV2Alpha).To(BeTrue()) }) It("returns true if v1-alpha plugin present", func() { cfg := &fakeConfig{plugins: map[string]any{"helm.kubebuilder.io/v1-alpha": true}} store := &fakeStore{cfg: cfg} hasPlugin, isV2Alpha := hasHelmPlugin(store) Expect(hasPlugin).To(BeTrue()) Expect(isV2Alpha).To(BeFalse()) }) It("returns false if both plugins not found", func() { cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: "helm.kubebuilder.io/v2-beta"}} store := &fakeStore{cfg: cfg} hasPlugin, isV2Alpha := hasHelmPlugin(store) Expect(hasPlugin).To(BeFalse()) Expect(isV2Alpha).To(BeFalse()) }) }) var _ = Describe("generate: migrate-plugins", func() { var ( kbc *utils.TestContext tmpDir string err error originalDir string ) BeforeEach(func() { // Initialize TestContext kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) // Setup mock kubebuilder environment originalDir = setupKubebuilderMockEnvironment(kbc) tmpDir = kbc.Dir }) AfterEach(func() { By("cleaning up test artifacts") // Restore original working directory Expect(os.Chdir(originalDir)).To(Succeed()) kbc.Destroy() }) Context("migrateGrafanaPlugin", func() { It("skips migration as Grafana plugin not found", func() { cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: "grafana.kubebuilder.io/v1-alpha"}} store := &fakeStore{cfg: cfg} Expect(migrateGrafanaPlugin(store, "src", "dest")).To(Succeed()) }) It("returns error if decoding Grafana plugin config fails", func() { cfg := &fakeConfig{ pluginErr: fmt.Errorf("decoding error"), plugins: map[string]any{"grafana.kubebuilder.io/v1-alpha": true}, } store := &fakeStore{cfg: cfg} Expect(migrateGrafanaPlugin(store, "src", "dest")).NotTo(Succeed()) }) Context("success", func() { var src, dest string BeforeEach(func() { src = filepath.Join(tmpDir, "src") dest = filepath.Join(tmpDir, "dest") Expect(os.MkdirAll(filepath.Join(src, "grafana/custom-metrics"), 0o755)).To(Succeed()) Expect(os.WriteFile(filepath.Join(src, "grafana/custom-metrics/config.yaml"), []byte("config"), 0o755)).To(Succeed()) Expect(os.MkdirAll(filepath.Join(dest, "grafana/custom-metrics"), 0o755)).To(Succeed()) }) AfterEach(func() { Expect(os.RemoveAll(src)).To(Succeed()) Expect(os.RemoveAll(dest)).To(Succeed()) }) It("migrates Grafana plugin successfully", func() { cfg := &fakeConfig{plugins: map[string]any{"grafana.kubebuilder.io/v1-alpha": true}} store := &fakeStore{cfg: cfg} Expect(migrateGrafanaPlugin(store, src, dest)).To(Succeed()) b, err := os.ReadFile(filepath.Join(dest, "grafana/custom-metrics/config.yaml")) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal("config")) }) }) }) Context("migrateAutoUpdatePlugin", func() { It("skips migration as AutoUpdate plugin not found", func() { cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: "autoupdate.kubebuilder.io/v1-alpha"}} store := &fakeStore{cfg: cfg} Expect(migrateAutoUpdatePlugin(store)).To(Succeed()) }) It("returns error if failed to decode Auto Update plugin", func() { cfg := &fakeConfig{ pluginErr: fmt.Errorf("decoding error"), plugins: map[string]any{"autoupdate.kubebuilder.io/v1-alpha": true}, } store := &fakeStore{cfg: cfg} Expect(migrateAutoUpdatePlugin(store)).NotTo(Succeed()) }) It("migrates Auto Update plugin successfully without UseGHModels", func() { cfg := &fakeConfig{ plugins: map[string]any{ "autoupdate.kubebuilder.io/v1-alpha": autoupdatev1alpha.PluginConfig{UseGHModels: false}, }, } store := &fakeStore{cfg: cfg} Expect(migrateAutoUpdatePlugin(store)).To(Succeed()) }) It("migrates Auto Update plugin successfully with UseGHModels enabled", func() { cfg := &fakeConfig{ plugins: map[string]any{ "autoupdate.kubebuilder.io/v1-alpha": autoupdatev1alpha.PluginConfig{UseGHModels: true}, }, } store := &fakeStore{cfg: cfg} Expect(migrateAutoUpdatePlugin(store)).To(Succeed()) }) }) Context("migrateDeployImagePlugin", func() { It("returns error if failed to decode Deploy Image plugin", func() { cfg := &fakeConfig{pluginErr: &config.PluginKeyNotFoundError{Key: "deploy-image.kubebuilder.io/v1-alpha"}} store := &fakeStore{cfg: cfg} Expect(migrateDeployImagePlugin(store)).To(Succeed()) }) It("returns error if decoding Deploy Image plugin config fails", func() { cfg := &fakeConfig{ pluginErr: fmt.Errorf("decoding error"), plugins: map[string]any{"deploy-image.kubebuilder.io/v1-alpha": true}, } store := &fakeStore{cfg: cfg} Expect(migrateDeployImagePlugin(store)).NotTo(Succeed()) }) It("migrates Deploy Image plugin successfully", func() { cfg := &fakeConfig{plugins: map[string]any{"deploy-image.kubebuilder.io/v1-alpha": true}} store := &fakeStore{cfg: cfg} // Mock resources for the plugin resources := []deployimagev1alpha1.ResourceData{ { Group: "example.com", Version: "v1", Kind: "Example", }, } cfg.pluginChain = []string{"deploy-image.kubebuilder.io/v1-alpha"} store.cfg = cfg // Use the mocked resources for _, r := range resources { Expect(createAPIWithDeployImage(r)).To(Succeed()) } Expect(migrateDeployImagePlugin(store)).To(Succeed()) }) }) }) var _ = Describe("Generate", func() { var ( kbc *utils.TestContext err error g *Generate originalDir string originalGetExecutablePathFunc func() (string, error) ) BeforeEach(func() { // Save the original function originalGetExecutablePathFunc = getExecutablePathFunc // Initialize TestContext kbc, err = utils.NewTestContext("kubebuilder", "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) // Setup mock kubebuilder environment originalDir = setupKubebuilderMockEnvironment(kbc) // Mock getExecutablePathFunc to return the mock kubebuilder binary path getExecutablePathFunc = func() (string, error) { return filepath.Join(kbc.Dir, "kubebuilder"), nil } // Initialize Generate g = &Generate{InputDir: kbc.Dir} }) AfterEach(func() { // Restore the original getExecutablePath function getExecutablePathFunc = originalGetExecutablePathFunc // Restore original working directory Expect(os.Chdir(originalDir)).To(Succeed()) By("cleaning up test artifacts") kbc.Destroy() }) Context("outputDir is non empty", func() { It("scaffolds the project in output dir", func() { g.OutputDir = kbc.Dir Expect(g.Generate()).To(Succeed()) }) }) }) ================================================ FILE: internal/cli/alpha/internal/update/helpers/conflict.go ================================================ /* Copyright 2025 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 helpers import ( "bufio" "bytes" "io/fs" log "log/slog" "os" "os/exec" "path/filepath" "slices" "strings" ) type ConflictSummary struct { Makefile bool // Makefile or makefile conflicted API bool // anything under api/ or apis/ conflicted AnyGo bool // any *.go file anywhere conflicted } // ConflictResult provides detailed conflict information for multiple use cases type ConflictResult struct { Summary ConflictSummary SourceFiles []string // conflicted source files GeneratedFiles []string // conflicted generated files } // isGeneratedKB returns true for Kubebuilder-generated artifacts. // Moved from open_gh_issue.go to avoid duplication func isGeneratedKB(path string) bool { return strings.Contains(path, "/zz_generated.") || strings.HasPrefix(path, "config/crd/bases/") || strings.HasPrefix(path, "config/rbac/") || path == "dist/install.yaml" || // Generated deepcopy files strings.HasSuffix(path, "_deepcopy.go") } // FindConflictFiles performs unified conflict detection for both conflict handling and GitHub issue generation func FindConflictFiles() ConflictResult { result := ConflictResult{ SourceFiles: []string{}, GeneratedFiles: []string{}, } // Use git index for fast conflict detection first gitConflicts := getGitIndexConflicts() // Filesystem scan for conflict markers fsConflicts := scanFilesystemForConflicts() // Combine results and categorize allConflicts := make(map[string]bool) for _, f := range gitConflicts { allConflicts[f] = true } for _, f := range fsConflicts { allConflicts[f] = true } // Categorize into source vs generated for file := range allConflicts { if isGeneratedKB(file) { result.GeneratedFiles = append(result.GeneratedFiles, file) } else { result.SourceFiles = append(result.SourceFiles, file) } } slices.Sort(result.SourceFiles) slices.Sort(result.GeneratedFiles) // Build summary for existing conflict.go usage result.Summary = ConflictSummary{ Makefile: hasConflictInFiles(allConflicts, "Makefile", "makefile"), API: hasConflictInPaths(allConflicts, "api", "apis"), AnyGo: hasGoConflictInFiles(allConflicts), } return result } // DetectConflicts maintains backward compatibility func DetectConflicts() ConflictSummary { return FindConflictFiles().Summary } // getGitIndexConflicts uses git ls-files to quickly find unmerged entries func getGitIndexConflicts() []string { out, err := exec.Command("git", "ls-files", "-u").Output() if err != nil { return nil } conflicts := make(map[string]bool) for line := range strings.SplitSeq(string(out), "\n") { fields := strings.Fields(line) if len(fields) >= 4 { file := strings.Join(fields[3:], " ") conflicts[file] = true } } result := make([]string, 0, len(conflicts)) for file := range conflicts { result = append(result, file) } return result } // scanFilesystemForConflicts scans the working directory for conflict markers func scanFilesystemForConflicts() []string { type void struct{} skipDir := map[string]void{ ".git": {}, "vendor": {}, "bin": {}, } const maxBytes = 2 << 20 // 2 MiB per file markersPrefix := [][]byte{ []byte("<<<<<<< "), []byte(">>>>>>> "), } markerExact := []byte("=======") var conflicts []string _ = filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { if err != nil { return nil // best-effort } // Skip unwanted directories if d.IsDir() { if _, ok := skipDir[d.Name()]; ok { return filepath.SkipDir } return nil } // Quick size check fi, err := d.Info() if err != nil { return nil } if fi.Size() > maxBytes { return nil } f, err := os.Open(path) if err != nil { return nil } defer func() { if cerr := f.Close(); cerr != nil { log.Warn("failed to close file", "path", path, "error", cerr) } }() found := false sc := bufio.NewScanner(f) // allow long lines (YAML/JSON) buf := make([]byte, 0, 1024*1024) sc.Buffer(buf, 4<<20) for sc.Scan() { b := sc.Bytes() // starts with conflict markers for _, p := range markersPrefix { if bytes.HasPrefix(b, p) { found = true break } } // exact middle marker line if !found && bytes.Equal(b, markerExact) { found = true } if found { break } } if found { conflicts = append(conflicts, path) } return nil }) return conflicts } // Helper functions for backward compatibility func hasConflictInFiles(conflicts map[string]bool, paths ...string) bool { for _, path := range paths { if conflicts[path] { return true } } return false } func hasConflictInPaths(conflicts map[string]bool, pathPrefixes ...string) bool { for file := range conflicts { for _, prefix := range pathPrefixes { if strings.HasPrefix(file, prefix+"/") || file == prefix { return true } } } return false } func hasGoConflictInFiles(conflicts map[string]bool) bool { for file := range conflicts { if strings.HasSuffix(file, ".go") { return true } } return false } // DecideMakeTargets applies simple policy over the summary. func DecideMakeTargets(cs ConflictSummary) []string { all := []string{"manifests", "generate", "fmt", "vet", "lint-fix"} if cs.Makefile { return nil } keep := make([]string, 0, len(all)) for _, t := range all { if cs.API && (t == "manifests" || t == "generate") { continue } if cs.AnyGo && (t == "fmt" || t == "vet" || t == "lint-fix") { continue } keep = append(keep, t) } return keep } ================================================ FILE: internal/cli/alpha/internal/update/helpers/conflict_test.go ================================================ /* Copyright 2025 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 helpers import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Conflict Detection", func() { Describe("isGeneratedKB", func() { It("should detect generated files", func() { Expect(isGeneratedKB("api/v1/zz_generated.deepcopy.go")).To(BeTrue()) Expect(isGeneratedKB("config/crd/bases/crew.testproject.org_captains.yaml")).To(BeTrue()) Expect(isGeneratedKB("config/rbac/role.yaml")).To(BeTrue()) Expect(isGeneratedKB("dist/install.yaml")).To(BeTrue()) Expect(isGeneratedKB("api/v1/captain_deepcopy.go")).To(BeTrue()) }) It("should not detect user files as generated", func() { Expect(isGeneratedKB("api/v1/captain_types.go")).To(BeFalse()) Expect(isGeneratedKB("internal/controller/captain_controller.go")).To(BeFalse()) Expect(isGeneratedKB("internal/webhook/v1/captain_webhook.go")).To(BeFalse()) Expect(isGeneratedKB("internal/controller/suite_test.go")).To(BeFalse()) Expect(isGeneratedKB("cmd/main.go")).To(BeFalse()) Expect(isGeneratedKB("Makefile")).To(BeFalse()) }) }) Describe("FindConflictFiles", func() { It("should return a valid ConflictResult structure", func() { result := FindConflictFiles() // Should have the expected structure Expect(result.SourceFiles).NotTo(BeNil()) Expect(result.GeneratedFiles).NotTo(BeNil()) Expect(result.Summary).To(Equal(ConflictSummary{ Makefile: result.Summary.Makefile, API: result.Summary.API, AnyGo: result.Summary.AnyGo, })) }) }) Describe("DetectConflicts", func() { It("should maintain backward compatibility", func() { summary := DetectConflicts() // Should return a valid ConflictSummary Expect(summary.Makefile).To(BeFalse()) // No conflicts in test environment Expect(summary.API).To(BeFalse()) Expect(summary.AnyGo).To(BeFalse()) }) }) Describe("DecideMakeTargets", func() { It("should return all targets when no conflicts", func() { cs := ConflictSummary{Makefile: false, API: false, AnyGo: false} targets := DecideMakeTargets(cs) expected := []string{"manifests", "generate", "fmt", "vet", "lint-fix"} Expect(targets).To(Equal(expected)) }) It("should return no targets when Makefile has conflicts", func() { cs := ConflictSummary{Makefile: true, API: false, AnyGo: false} targets := DecideMakeTargets(cs) Expect(targets).To(BeNil()) }) It("should skip API targets when API has conflicts", func() { cs := ConflictSummary{Makefile: false, API: true, AnyGo: false} targets := DecideMakeTargets(cs) expected := []string{"fmt", "vet", "lint-fix"} Expect(targets).To(Equal(expected)) }) It("should skip Go targets when Go files have conflicts", func() { cs := ConflictSummary{Makefile: false, API: false, AnyGo: true} targets := DecideMakeTargets(cs) expected := []string{"manifests", "generate"} Expect(targets).To(Equal(expected)) }) }) }) ================================================ FILE: internal/cli/alpha/internal/update/helpers/download.go ================================================ /* Copyright 2025 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 helpers import ( "context" "fmt" "io" log "log/slog" "net/http" "os" "path/filepath" "runtime" "time" "github.com/spf13/afero" ) const KubebuilderReleaseURL = "https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s" func BuildReleaseURL(version string) string { return fmt.Sprintf(KubebuilderReleaseURL, version, runtime.GOOS, runtime.GOARCH) } // DownloadReleaseVersionWith downloads the specified released version from GitHub releases and saves it // to a temporary directory with executable permissions. // Returns the temporary directory path containing the binary. func DownloadReleaseVersionWith(version string) (string, error) { url := BuildReleaseURL(version) // Create temp directory fs := afero.NewOsFs() tempDir, err := afero.TempDir(fs, "", "kubebuilder"+version+"-") if err != nil { return "", fmt.Errorf("failed to create temporary directory: %w", err) } // Ensure cleanup on any error after this point cleanupOnErr := func() { if rmErr := os.RemoveAll(tempDir); rmErr != nil { log.Error("failed to remove temporary directory", "dir", tempDir, "error", rmErr) } } binaryPath := filepath.Join(tempDir, "kubebuilder") f, err := fs.Create(binaryPath) if err != nil { cleanupOnErr() return "", fmt.Errorf("failed to create the binary file: %w", err) } defer func() { if closeErr := f.Close(); closeErr != nil { log.Error("failed to close the binary file", "error", closeErr) } }() ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { cleanupOnErr() return "", fmt.Errorf("failed to build download request: %w", err) } req.Header.Set("User-Agent", "kubebuilder-updater/1.0 (+https://github.com/kubernetes-sigs/kubebuilder)") resp, err := http.DefaultClient.Do(req) if err != nil { cleanupOnErr() return "", fmt.Errorf("failed to download the binary: %w", err) } defer func() { if closeErr := resp.Body.Close(); closeErr != nil { log.Error("failed to close HTTP response body", "error", closeErr) } }() if resp.StatusCode != http.StatusOK { cleanupOnErr() return "", fmt.Errorf("failed to download the binary: HTTP %d", resp.StatusCode) } if _, err := io.Copy(f, resp.Body); err != nil { cleanupOnErr() return "", fmt.Errorf("failed to write the binary content to file: %w", err) } // Flush to disk before changing mode (best effort) if syncErr := f.Sync(); syncErr != nil { log.Warn("failed to sync binary to disk (continuing)", "error", syncErr) } if err := os.Chmod(binaryPath, 0o755); err != nil { cleanupOnErr() return "", fmt.Errorf("failed to make binary executable: %w", err) } return tempDir, nil } ================================================ FILE: internal/cli/alpha/internal/update/helpers/download_test.go ================================================ /* Copyright 2025 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 helpers import ( "errors" "fmt" "os" "path/filepath" "runtime" "strings" "github.com/h2non/gock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("helpers", func() { AfterEach(func() { gock.Off() // ensure HTTP mocks are cleared between tests }) Context("BuildReleaseURL", func() { It("builds the exact URL for the current OS/ARCH", func() { v := "v4.5.0" expected := fmt.Sprintf(KubebuilderReleaseURL, v, runtime.GOOS, runtime.GOARCH) Expect(BuildReleaseURL(v)).To(Equal(expected)) }) }) Context("DownloadReleaseVersionWith", func() { const version = "v4.6.0" It("downloads the binary and makes it executable", func() { // Arrange: mock the GitHub release endpoint url := BuildReleaseURL(version) parts := strings.SplitN(url, "/", 4) Expect(parts).To(HaveLen(4)) host := parts[0] + "//" + parts[2] path := "/" + parts[3] gock.New(host). Get(path). Reply(200). BodyString("#!/bin/sh\necho kubebuilder\n") dir, err := DownloadReleaseVersionWith(version) Expect(err).NotTo(HaveOccurred()) Expect(dir).NotTo(BeEmpty()) bin := filepath.Join(dir, "kubebuilder") st, statErr := os.Stat(bin) Expect(statErr).NotTo(HaveOccurred()) Expect(st.Mode().IsRegular()).To(BeTrue()) if runtime.GOOS != "windows" { Expect(st.Mode() & 0o111).NotTo(BeZero()) } }) It("returns a clear error when the server responds non-200", func() { url := BuildReleaseURL(version) parts := strings.SplitN(url, "/", 4) host := parts[0] + "//" + parts[2] path := "/" + parts[3] gock.New(host). Get(path). Reply(401). BodyString("") _, err := DownloadReleaseVersionWith(version) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to download the binary: HTTP 401")) }) It("propagates network errors when the request fails", func() { url := BuildReleaseURL(version) parts := strings.SplitN(url, "/", 4) host := parts[0] + "//" + parts[2] path := "/" + parts[3] gock.New(host). Get(path). ReplyError(errors.New("boom")) _, err := DownloadReleaseVersionWith(version) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to download the binary:")) Expect(err.Error()).To(ContainSubstring("boom")) }) }) }) ================================================ FILE: internal/cli/alpha/internal/update/helpers/git_commands.go ================================================ /* Copyright 2025 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 helpers import ( "errors" "fmt" "log/slog" "os/exec" ) // CommitIgnoreEmpty commits the staged changes with the provided message. func CommitIgnoreEmpty(msg, ctx string) error { cmd := exec.Command("git", "commit", "--no-verify", "-m", msg) if err := cmd.Run(); err != nil { var ee *exec.ExitError if errors.As(err, &ee) && ee.ExitCode() == 1 { // nothing to commit slog.Info("No changes to commit", "context", ctx, "message", msg) return nil } return fmt.Errorf("git commit failed (%s): %w", ctx, err) } return nil } // CleanWorktree removes everything in the repo root except .git so the next // checkout writes a verbatim snapshot of the source branch. func CleanWorktree(label string) error { if err := exec.Command("sh", "-c", "find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +").Run(); err != nil { return fmt.Errorf("cleanup for %s: %w", label, err) } return nil } // GitCmd creates a new git command with the provided git configuration func GitCmd(gitConfig []string, args ...string) *exec.Cmd { gitArgs := make([]string, 0, len(gitConfig)*2+len(args)) for _, kv := range gitConfig { gitArgs = append(gitArgs, "-c", kv) } gitArgs = append(gitArgs, args...) return exec.Command("git", gitArgs...) } // MergeCommitMessage returns the commit message for a successful merge update func MergeCommitMessage(from, to string) string { return fmt.Sprintf("chore(kubebuilder): update scaffold %s -> %s", from, to) } // ConflictCommitMessage returns the commit message for a merge update with conflicts func ConflictCommitMessage(from, to string) string { //nolint:lll return fmt.Sprintf("chore(kubebuilder): (:warning: manual conflict resolution required) update scaffold %s -> %s", from, to) } ================================================ FILE: internal/cli/alpha/internal/update/helpers/open_gh_issue.go ================================================ /* Copyright 2025 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 helpers import ( "bufio" "bytes" "fmt" "os/exec" "regexp" "slices" "strings" ) // Curated-diff budgets (fixed; no env vars) const ( selectedDiffTotalCap = 96 << 10 // 96 KiB total across all files selectedDiffLinesPerFile = 120 // default +/- lines per file selectedDiffLinesGoMod = 240 // allow more for go.mod ) // IssueTitleTmpl is the title template for the GitHub issue. const IssueTitleTmpl = "[Action Required] Upgrade the Scaffold: %[2]s -> %[1]s" // IssueBodyTmpl is used when no conflicts are detected during the merge. // //nolint:lll const IssueBodyTmpl = `## Description Upgrade your project to use the latest scaffold changes introduced in Kubebuilder [%[1]s](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%[1]s). See the release notes from [%[3]s](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%[3]s) to [%[1]s](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%[1]s) for details about the changes included in this upgrade. ## What to do A scheduled workflow already attempted this upgrade and created the branch %[4]s to help you in this process. Create a Pull Request using the URL below to review the changes: %[2]s ## Next steps **Verify the changes** - Build the project - Run tests - Confirm everything still works :book: **More info:** https://kubebuilder.io/reference/commands/alpha_update ` // IssueBodyTmplWithConflicts is used when conflicts are detected during the merge. // //nolint:lll const IssueBodyTmplWithConflicts = `## Description Upgrade your project to use the latest scaffold changes introduced in Kubebuilder [%[1]s](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%[1]s). See the release notes from [%[3]s](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%[3]s) to [%[1]s](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%[1]s) for details about the changes included in this upgrade. ## What to do A scheduled workflow already attempted this upgrade and created the branch (%[4]s) to help you in this process. :warning: **Conflicts were detected during the merge.** Create a Pull Request using the URL below to review the changes and resolve conflicts manually: %[2]s ## Next steps ### 1. Resolve conflicts After fixing conflicts, run: ~~~bash make manifests generate fmt vet lint-fix ~~~ ### 2. Optional: work on a new branch To apply the update in a clean branch, run: ~~~bash kubebuilder alpha update --output-branch my-fix-branch ~~~ This will create a new branch (my-fix-branch) with the update applied. Resolve conflicts there, complete the merge locally, and push the branch. ### 3. Verify the changes - Build the project - Run tests - Confirm everything still works :book: **More info:** https://kubebuilder.io/reference/commands/alpha_update ` // AiPRPrompt is the prompt to `gh models run`. // //nolint:lll const AiPRPrompt = `You are a senior Go/K8s engineer. Produce a concise, reviewer-friendly **Pull Request summary** for a Kubebuilder project upgrade. Style rules: - Use **simple, plain English** (like Kubebuilder docs). - Avoid jargon or long sentences. - Focus on clarity and readability for new contributors. Rules (follow strictly): - Do NOT output angle-bracket placeholders like ; use the real value from the context. - Do NOT guess versions. Only mention an exact version (e.g., controller-runtime v0.21.0) if that exact version string appears in the provided diffs/context (e.g., go.mod). - When talking about dependencies: - **Only** name modules that changed on **non-indirect** ` + "`require`" + ` lines in **go.mod** (i.e., lines **without** "// indirect"). - You may also name explicit tool versions found in **Makefile** or **Dockerfile** (e.g., controller-tools, golangci-lint, Go toolchain). - **Never** name modules that appear only with "// indirect" or only in **go.sum** or generated files. - If you cannot name any direct modules safely, write simply: "dependencies updated" (no module names). - Output exactly one overview and one reviewed-changes table. No duplicates. - Valid Markdown only. No ">>>", no meta commentary. - Start with this exact sentence, substituting real values: "This is a Kubebuilder scaffold update from %s to %s on branch %s." If a Compare PR URL is provided in the context header, append it **in parentheses** at the end of that sentence as a Markdown link, e.g., " (see [compare PR](URL))". - A "conflict" means the file currently contains Git merge markers (<<<<<<<, =======, >>>>>>>) and requires manual resolution. If no conflicts are provided in the context, omit the conflicts section entirely. - Conflicts section: ONLY add if there are conflicts. Do NOT invent conflicts. - Do NOT invent changes; use only what is in the context. Required sections (Markdown, EXACT wording/case): ## ( :robot: AI generate ) Scaffold Changes Overview Start with one short sentence: "This is a Kubebuilder scaffold update from to on branch ." (with the optional compare link in parentheses at the end). Then list 4–6 concise bullet highlights (e.g., Go/tooling bumps, controller-runtime/k8s.io deps, security hardening like readOnlyRootFilesystem, error handling improvements). Then list **only the most important 6–10 bullet points** (never more than 10 items total in this section). If there are many changes, summarize and cluster them (e.g., "several small Go tooling bumps") instead of listing everything. ### ( :robot: AI generate ) Reviewed Changes Add a collapsible block:
Show a summary per file | File | Description | | ---- | ----------- | | … | … |
Build the table using ONLY the "Changed files" lists provided in the context. Do not invent files. It is OK if some files also appear in the Conflicts section. If there are many GENERATED files, you may **group them** using a glob with a count (e.g., ` + "`config/crd/bases/*.yaml (12 files)`" + `) instead of listing each one. **ONLY** if the context includes conflict files; add ANOTHER collapsible block titled **Conflicts Summary**:
Conflicts Summary | File | Description | | ---- | ----------- | | … | … |
A "conflict" means the file currently contains Git merge markers (<<<<<<<, =======, >>>>>>>) and requires manual resolution. If no conflicts are provided in the context, omit this section. List each conflicted file with a brief suggestion. For GENERATED files: - api/**/zz_generated.*.go: "Do not edit by hand; run: make generate" - config/crd/bases/*.yaml: "Fix types in api/*_types.go; then run: make manifests" - config/rbac/*.yaml: "Fix markers in controllers/webhooks; then run: make manifests" - dist/install.yaml: "Fix conflicts; then run: make build-installer"` // listConflictFiles uses the unified conflict detection from conflict.go func listConflictFiles() (src []string, gen []string) { conflicts := FindConflictFiles() return conflicts.SourceFiles, conflicts.GeneratedFiles } func bulletList(items []string) string { if len(items) == 0 { return "" } return "- " + strings.Join(items, "\n- ") } // FirstURL is a helper to grab the first URL-looking token from gh stdout func FirstURL(s string) string { for f := range strings.FieldsSeq(s) { if strings.HasPrefix(f, "http://") || strings.HasPrefix(f, "https://") { // trim common trailing punctuation return strings.TrimRight(f, ").,") } } return "" } // IssueNumberFromURL returns the last path segment (…/issues/). func IssueNumberFromURL(u string) string { u = strings.TrimSuffix(u, "/") if i := strings.LastIndex(u, "/"); i >= 0 && i+1 < len(u) { return u[i+1:] } return "" } // listChangedFiles returns files changed between base..head, split into SOURCE and GENERATED. func listChangedFiles(base, head string) (src []string, gen []string) { cmd := exec.Command("git", "diff", "--name-only", "-M", "--diff-filter=ACMRTD", base+".."+head) out, err := cmd.Output() if err != nil { return nil, nil // best-effort } for p := range strings.SplitSeq(strings.TrimSpace(string(out)), "\n") { p = strings.TrimSpace(p) if p == "" { continue } if isGeneratedKB(p) { gen = append(gen, p) } else { src = append(src, p) } } slices.Sort(src) slices.Sort(gen) return src, gen } // BuildFullPrompet builds the AI context and writes it to a temp file. // It returns the absolute filepath to pass via --input-file/--file. func BuildFullPrompet( fromVersion, toVersion, baseBranch, outBranch, compareURL, releaseURL string, ) string { changedSrc, changedGen := listChangedFiles(baseBranch, outBranch) conflictSrc, conflictGen := listConflictFiles() var ctx strings.Builder fmt.Fprintf(&ctx, "Kubebuilder upgrade: %s -> %s\n", fromVersion, toVersion) fmt.Fprintf(&ctx, "Compare PR URL: %s\n", compareURL) fmt.Fprintf(&ctx, "Release notes: %s\n\n", releaseURL) ctx.WriteString("\n") // List changed files so the AI can build the Reviewed Changes table. if len(changedSrc) > 0 { fmt.Fprintf(&ctx, "\nChanged [SOURCE] files:\n%s\n", bulletList(changedSrc)) } if len(changedGen) > 0 { fmt.Fprintf(&ctx, "\nChanged [GENERATED] files:\n%s\n", bulletList(changedGen)) } // List conflicts for extra context (will be empty if none) if len(conflictSrc) > 0 { fmt.Fprintf(&ctx, "\nConflicted [SOURCE] files:\n%s\n", bulletList(conflictSrc)) } if len(conflictGen) > 0 { fmt.Fprintf(&ctx, "\nConflicted [GENERATED] files:\n%s\n", bulletList(conflictGen)) } // Concise, curated diffs for important SOURCE files only if len(changedSrc) > 0 { ctx.WriteString("## Selected diffs\n") // Per-file cap is ignored for go.mod (it uses its own higher cap). const perFileLineCap = selectedDiffLinesPerFile // total cap is fixed inside concatSelectedDiffs (selectedDiffTotalCap). ctx.WriteString(concatSelectedDiffs(strings.TrimSpace(baseBranch), strings.TrimSpace(outBranch), changedSrc, perFileLineCap, selectedDiffTotalCap)) ctx.WriteString("\n") } return ctx.String() } // Never include these in curated diffs. func excludedFromDiff(p string) bool { return isGeneratedKB(p) || strings.HasSuffix(p, ".md") || p == "PROJECT" || p == "go.sum" || strings.HasPrefix(p, "grafana/") || strings.HasPrefix(p, "config/crd/bases/") || strings.HasPrefix(p, "hack/") || strings.HasPrefix(p, "bin/") || strings.HasPrefix(p, "vendor/") || strings.HasSuffix(p, ".log") } // Only files that matter for KB review context (after exclusions). func importantFile(p string) bool { if excludedFromDiff(p) { return false } // Critical Kubebuilder files //nolint:goconst if p == "go.mod" || p == "Makefile" || p == "Dockerfile" { return true } // Core source code if strings.HasPrefix(p, "cmd/") || strings.HasPrefix(p, "controllers/") || strings.HasPrefix(p, "internal/controller/") || strings.HasPrefix(p, "internal/webhook/") || (strings.HasPrefix(p, "api/") && strings.HasSuffix(p, "_types.go")) { return true } // Test files (important for breaking changes) if strings.HasPrefix(p, "test/") && (strings.HasSuffix(p, "_test.go") || strings.HasSuffix(p, ".go")) { return true } // Important config files (not generated) if strings.HasPrefix(p, "config/") { // Include kustomization files and important config if strings.HasSuffix(p, "kustomization.yaml") || strings.HasPrefix(p, "config/default/") || strings.HasPrefix(p, "config/manager/") || strings.HasPrefix(p, "config/webhook/") || strings.HasPrefix(p, "config/certmanager/") || strings.HasPrefix(p, "config/prometheus/") || strings.HasPrefix(p, "config/network-policy/") { return true } } return false } // Priority: lower number = earlier. // 0: go.mod (dependencies) // 1: Makefile (build automation) // 2: Dockerfile (container images) // 3: Core code (cmd/, controllers/, api/*_types.go, internal/) // 4: Critical config (config/default, config/manager) // 5: Webhook & security config (config/webhook, config/certmanager) // 6: Other config (config/*) // 7: Tests // 9: fallback func filePriority(p string) int { switch { case p == "go.mod": return 0 case p == "Makefile": return 1 case p == "Dockerfile": return 2 case strings.HasPrefix(p, "cmd/"), strings.HasPrefix(p, "controllers/"), strings.HasPrefix(p, "internal/controller/"), strings.HasPrefix(p, "internal/webhook/"), (strings.HasPrefix(p, "api/") && strings.HasSuffix(p, "_types.go")): return 3 case strings.HasPrefix(p, "config/default/"), strings.HasPrefix(p, "config/manager/"), p == "config/default/kustomization.yaml", p == "config/manager/kustomization.yaml": return 4 case strings.HasPrefix(p, "config/webhook/"), strings.HasPrefix(p, "config/certmanager/"), strings.HasPrefix(p, "config/prometheus/"), strings.HasPrefix(p, "config/network-policy/"): return 5 case strings.HasPrefix(p, "config/"): return 6 case strings.HasPrefix(p, "test/"): return 7 default: return 9 } } //nolint:lll var ( reFlags = regexp.MustCompile(`(?i)--(leader-elect|metrics-bind-address|health-probe-bind-address|\bzap|secure-port|bind-address)`) reGo = regexp.MustCompile(`(?i)^(?:\+|\-)\s*(package|import|type|func|const|var|//\+kubebuilder:|//go:(?:build|generate)|return|if\s+err|log\.|fmt\.|errors?\.|client\.|ctrl\.|manager|scheme|requeue|context\.|SetupWithManager|Reconcile|reconcile\.Result)`) reYAMLKey = regexp.MustCompile(`(?i)(apiVersion:|kind:|metadata:|name:|namespace:|image:|command:|args:|env:|resources:|limits:|requests:|ports:|securityContext:|readOnlyRootFilesystem|runAsNonRoot|seccompProfile|allowPrivilegeEscalation|capabilities|livenessProbe|readinessProbe|namePrefix:|commonLabels:|bases:|patches:|replicas:)`) reDocker = regexp.MustCompile(`(?i)^(?:\+|\-)\s*(FROM|ARG|ENV|RUN|ENTRYPOINT|CMD|COPY|ADD|USER|WORKDIR)\b`) reMakeLine = regexp.MustCompile(`(?i)^(?:\+|\-)\s*([A-Z0-9_]+)\s*[:?+]?=\s*|^(?:\+|\-)\s*(manifests|generate|fmt|vet|lint-fix|docker-build|test|install|uninstall|deploy|undeploy|build-installer|controller-gen|kustomize)\b`) reKubebuilder = regexp.MustCompile(`(?i)^(?:\+|\-)\s*(\/\/\+kubebuilder:|kubebuilder\s+(init|create|edit)|controller-runtime|sigs\.k8s\.io|k8s\.io\/api|k8s\.io\/apimachinery)`) ) // keepGoModLine returns true for +/- go.mod lines we want to retain. // Keep: module/go/toolchain, replace, require lines without "// indirect", and block delimiters. func keepGoModLine(s string) bool { if len(s) == 0 || (s[0] != '+' && s[0] != '-') { return false } t := strings.TrimSpace(s[1:]) // strip +/- then trim switch { case strings.HasPrefix(t, "module "): return true case strings.HasPrefix(t, "go "): return true case strings.HasPrefix(t, "toolchain "): return true case strings.HasPrefix(t, "replace "): return true case strings.HasPrefix(t, "require ") && !strings.Contains(t, "// indirect"): return true case t == "require (" || t == ")": // keep block delimiters for readability return true default: return false } } // Decide if a +/- line is interesting based on the file path. func interestingLine(path, line string) bool { if len(line) == 0 || (line[0] != '+' && line[0] != '-') { return false } switch { case strings.HasSuffix(path, ".go"): return reGo.MatchString(line) || reKubebuilder.MatchString(line) case strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml"): return reYAMLKey.MatchString(line) || reFlags.MatchString(line) || reKubebuilder.MatchString(line) case path == "Makefile": return reMakeLine.MatchString(line) || reKubebuilder.MatchString(line) case path == "Dockerfile": return reDocker.MatchString(line) case strings.HasSuffix(path, "kustomization.yaml"): // Kustomization files are critical for Kubebuilder config return true default: // Unknown text files: keep Kubebuilder-related lines and obvious flag changes return reFlags.MatchString(line) || reKubebuilder.MatchString(line) } } // Curated unified=0 diff: keep hunk headers + filtered +/- lines. // For go.mod keep only direct requires and key headers (still capped). func selectedDiff(base, head, path string, maxLines int) string { cmd := exec.Command("git", "diff", "--no-color", "-w", "--unified=0", base+".."+head, "--", path) out, _ := cmd.Output() if len(out) == 0 { return "" } sc := bufio.NewScanner(bytes.NewReader(out)) lines := 0 var b strings.Builder if path == "go.mod" { for sc.Scan() { s := sc.Text() if strings.HasPrefix(s, "@@") { b.WriteString(s + "\n") continue } if keepGoModLine(s) { b.WriteString(s + "\n") lines++ if lines >= maxLines { break } } } return strings.TrimSpace(b.String()) } for sc.Scan() { s := sc.Text() if strings.HasPrefix(s, "@@") { b.WriteString(s + "\n") continue } if len(s) == 0 || (s[0] != '+' && s[0] != '-') { continue } if interestingLine(path, s) { b.WriteString(s + "\n") lines++ if lines >= maxLines { break } } } return strings.TrimSpace(b.String()) } func concatSelectedDiffs(base, head string, files []string, perFileLineCap, totalByteCap int) string { var b strings.Builder // Global budget: prefer the passed-in cap if >0, else default. remaining := totalByteCap if remaining <= 0 { remaining = selectedDiffTotalCap } // Filter and prioritize candidates candidates := make([]string, 0, len(files)) for _, p := range files { if importantFile(p) { candidates = append(candidates, p) } } slices.SortStableFunc(candidates, func(a, b string) int { pi, pj := filePriority(a), filePriority(b) if pi != pj { return pi - pj } return strings.Compare(a, b) // stable alphabetical within same priority }) // Emit diffs until the global budget is hit for _, p := range candidates { // Per-file line budget: use param if >0, else default; ensure go.mod gets at least its larger cap. perCap := perFileLineCap if perCap <= 0 { perCap = selectedDiffLinesPerFile } if p == "go.mod" && perCap < selectedDiffLinesGoMod { perCap = selectedDiffLinesGoMod } diff := selectedDiff(base, head, p, perCap) if diff == "" { continue } section := "----- BEGIN SELECTED DIFF " + p + " -----\n" + diff + "\n----- END SELECTED DIFF " + p + " -----\n\n" if len(section) > remaining { if remaining <= 0 { b.WriteString("\n... [global diff budget reached] ...\n") break } // Trim last section to fit remaining budget cut := min(remaining, len(section)) b.WriteString(section[:cut]) b.WriteString("\n... [global diff budget reached] ...\n") break } b.WriteString(section) remaining -= len(section) } out := strings.TrimSpace(b.String()) if out == "" { return "" } return out } ================================================ FILE: internal/cli/alpha/internal/update/helpers/open_gh_issue_test.go ================================================ /* Copyright 2025 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 helpers import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Open GitHub Issue Helpers", func() { Describe("excludedFromDiff", func() { It("should exclude unimportant files", func() { Expect(excludedFromDiff("PROJECT")).To(BeTrue()) Expect(excludedFromDiff("README.md")).To(BeTrue()) Expect(excludedFromDiff("hack/boilerplate.go.txt")).To(BeTrue()) Expect(excludedFromDiff("go.sum")).To(BeTrue()) }) It("should include important files", func() { Expect(excludedFromDiff("go.mod")).To(BeFalse()) Expect(excludedFromDiff("Makefile")).To(BeFalse()) Expect(excludedFromDiff("Dockerfile")).To(BeFalse()) }) }) Describe("importantFile", func() { It("should identify important core files", func() { Expect(importantFile("go.mod")).To(BeTrue()) Expect(importantFile("Makefile")).To(BeTrue()) Expect(importantFile("cmd/main.go")).To(BeTrue()) Expect(importantFile("api/v1/captain_types.go")).To(BeTrue()) }) It("should exclude generated and unimportant files", func() { Expect(importantFile("PROJECT")).To(BeFalse()) Expect(importantFile("hack/boilerplate.go.txt")).To(BeFalse()) Expect(importantFile("config/crd/bases/captain.yaml")).To(BeFalse()) }) }) Describe("filePriority", func() { It("should prioritize files correctly", func() { Expect(filePriority("go.mod")).To(Equal(0)) Expect(filePriority("Makefile")).To(Equal(1)) Expect(filePriority("Dockerfile")).To(Equal(2)) Expect(filePriority("cmd/main.go")).To(Equal(3)) Expect(filePriority("config/default/kustomization.yaml")).To(Equal(4)) }) }) Describe("FirstURL", func() { It("should extract URLs from text", func() { Expect(FirstURL("https://github.com/user/repo")).To(Equal("https://github.com/user/repo")) Expect(FirstURL("Check https://example.com here")).To(Equal("https://example.com")) Expect(FirstURL("no links here")).To(Equal("")) }) }) Describe("IssueNumberFromURL", func() { It("should extract issue numbers", func() { Expect(IssueNumberFromURL("https://github.com/user/repo/issues/123")).To(Equal("123")) Expect(IssueNumberFromURL("https://github.com/user/repo/pull/456")).To(Equal("456")) }) }) Describe("bulletList", func() { It("should format bullet lists", func() { Expect(bulletList([]string{})).To(Equal("")) Expect(bulletList([]string{"item1"})).To(Equal("- item1")) Expect(bulletList([]string{"item1", "item2"})).To(Equal("- item1\n- item2")) }) }) Describe("keepGoModLine", func() { It("should keep important go.mod lines", func() { Expect(keepGoModLine("+module github.com/user/repo")).To(BeTrue()) Expect(keepGoModLine("+go 1.21")).To(BeTrue()) Expect(keepGoModLine("+require example.com/pkg v1.0.0")).To(BeTrue()) }) It("should skip indirect dependencies", func() { Expect(keepGoModLine("+require example.com/pkg v1.0.0 // indirect")).To(BeFalse()) }) }) Describe("interestingLine", func() { It("should detect interesting Go lines", func() { Expect(interestingLine("main.go", "+import \"context\"")).To(BeTrue()) Expect(interestingLine("controller.go", "+//+kubebuilder:rbac:groups=apps")).To(BeTrue()) }) It("should detect interesting YAML lines", func() { Expect(interestingLine("manager.yaml", "+apiVersion: apps/v1")).To(BeTrue()) Expect(interestingLine("config.yaml", "+image: controller:latest")).To(BeTrue()) }) It("should skip uninteresting lines", func() { Expect(interestingLine("main.go", "+x := 1")).To(BeFalse()) Expect(interestingLine("config.yaml", "+# comment")).To(BeFalse()) }) }) }) ================================================ FILE: internal/cli/alpha/internal/update/helpers/suite_test.go ================================================ /* Copyright 2025 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 helpers import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestCommand(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "alpha command: update helpers suite") } ================================================ FILE: internal/cli/alpha/internal/update/integration_test.go ================================================ //go:build integration /* Copyright 2025 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 update import ( "bytes" "fmt" "io" "net/http" "os" "os/exec" "path/filepath" "runtime" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/update/helpers" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/test/e2e/utils" ) const ( fromVersion = "v4.5.2" toVersion = "v4.6.0" toVersionWithConflict = "v4.7.0" // Memcached operator + all plugins integration test (mirrors testdata generate.sh with-plugins) // Upgrade from a fixed past version to current (latest release when test runs). memcachedFromVersion = "v4.11.1" // Custom registry value added to Helm values.yaml; must be preserved after alpha update. memcachedHelmCustomRegistry = "myregistry.io/custom/controller" // Regex matching the commented Affinity block in deploy-image memcached controller (any whitespace). // Used with plugin util ReplaceRegexInFile so we can use backtick replacement string. memcachedAffinityCommentedRegex = `(?s)// TODO\(user\): Uncomment the following code to configure the nodeAffinity expression.*?//\s*\},` // Uncommented Affinity block (customization to be preserved by alpha update). memcachedAffinityUncommented = `// Node affinity for multi-arch (customization preserved by update) Affinity: &corev1.Affinity{ NodeAffinity: &corev1.NodeAffinity{ RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ NodeSelectorTerms: []corev1.NodeSelectorTerm{{ MatchExpressions: []corev1.NodeSelectorRequirement{ {Key: "kubernetes.io/arch", Operator: "In", Values: []string{"amd64", "arm64", "ppc64le", "s390x"}}, {Key: "kubernetes.io/os", Operator: "In", Values: []string{"linux"}}, }, }}, }, }, },` controllerImplementation = `// Fetch the TestOperator instance testOperator := &webappv1.TestOperator{} err := r.Get(ctx, req.NamespacedName, testOperator) if err != nil { if errors.IsNotFound(err) { log.Info("testOperator resource not found. Ignoring since object must be deleted") return ctrl.Result{}, nil } log.Error(err, "Failed to get testOperator") return ctrl.Result{}, err } log.Info("testOperator reconciled")` customField = `// +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:Maximum=3 // +kubebuilder:default=1 Size int32 ` + "`json:\"size,omitempty\"`" + ` ` ) var _ = Describe("kubebuilder", func() { Context("alpha update", func() { var ( pathBinFromVersion string kbc *utils.TestContext ) BeforeEach(func() { var err error By("setting up test context with binary build from source") kbc, err = utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) pathBinFromVersion, err = downloadKubebuilderVersion(fromVersion) Expect(err).NotTo(HaveOccurred()) cmd := exec.Command(pathBinFromVersion, "init", "--domain", "example.com", "--repo", "github.com/example/test-operator") cmd.Dir = kbc.Dir output, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("init failed: %s", output)) cmd = exec.Command(pathBinFromVersion, "create", "api", "--group", "webapp", "--version", "v1", "--kind", "TestOperator", "--resource", "--controller") cmd.Dir = kbc.Dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("create api failed: %s", output)) Expect(kbc.Make("generate", "manifests")).To(Succeed()) updateAPI(kbc.Dir) updateController(kbc.Dir) initializeGitRepo(kbc.Dir) }) AfterEach(func() { By("cleaning up test artifacts") _ = os.RemoveAll(filepath.Dir(pathBinFromVersion)) _ = os.RemoveAll(kbc.Dir) kbc.Destroy() }) It("should update project from v4.5.2 to v4.6.0 without conflicts", func() { By("running alpha update from v4.5.2 to v4.6.0") cmd := exec.Command( kbc.BinaryName, "alpha", "update", "--from-version", fromVersion, "--to-version", toVersion, "--from-branch", "main", ) cmd.Dir = kbc.Dir out, err := kbc.Run(cmd) Expect(err).NotTo(HaveOccurred(), string(out)) By("checking that custom code is preserved") validateCustomCodePreservation(kbc.Dir) By("checking that no conflict markers are present in the project files") Expect(hasConflictMarkers(kbc.Dir)).To(BeFalse()) By("checking that go module is upgraded") validateCommonGoModule(kbc.Dir) By("checking that Makefile is updated") validateMakefileContent(kbc.Dir) By("checking temporary branches were cleaned up locally") outRefs, err := exec.Command("git", "-C", kbc.Dir, "for-each-ref", "--format=%(refname:short)", "refs/heads").CombinedOutput() Expect(err).NotTo(HaveOccurred(), string(outRefs)) Expect(string(outRefs)).NotTo(ContainSubstring("tmp-ancestor")) Expect(string(outRefs)).NotTo(ContainSubstring("tmp-original")) Expect(string(outRefs)).NotTo(ContainSubstring("tmp-upgrade")) Expect(string(outRefs)).NotTo(ContainSubstring("tmp-merge")) }) It("should update project from v4.5.2 to v4.7.0 with --force flag and create conflict markers", func() { By("modifying original Makefile to use CONTROLLER_TOOLS_VERSION v0.17.3") modifyMakefileControllerTools(kbc.Dir, "v0.17.3") By("running alpha update with --force (default behavior is squash)") cmd := exec.Command( kbc.BinaryName, "alpha", "update", "--from-version", fromVersion, "--to-version", toVersionWithConflict, "--from-branch", "main", "--force", ) cmd.Dir = kbc.Dir out, err := kbc.Run(cmd) Expect(err).NotTo(HaveOccurred(), string(out)) By("checking that custom code is preserved") validateCustomCodePreservation(kbc.Dir) By("checking that conflict markers are present in the project files") Expect(hasConflictMarkers(kbc.Dir)).To(BeTrue()) By("checking that go module is upgraded to expected versions") validateCommonGoModule(kbc.Dir) By("checking that Makefile is updated and has conflict between old and new versions in Makefile") makefilePath := filepath.Join(kbc.Dir, "Makefile") content, err := os.ReadFile(makefilePath) Expect(err).NotTo(HaveOccurred(), "Failed to read Makefile after update") makefileStr := string(content) // Should update to the new version Expect(makefileStr).To(ContainSubstring(`GOLANGCI_LINT_VERSION ?= v2.1.6`)) // The original project was scaffolded with v0.17.2 (from v4.5.2). // The user manually updated it to v0.17.3. // The target upgrade version (v4.7.0) introduces v0.18.0. // // Because both the user's version (v0.17.3) and the scaffold version (v0.18.0) differ, // we expect Git to insert conflict markers around this line in the Makefile: // // <<<<<<< HEAD // CONTROLLER_TOOLS_VERSION ?= v0.18.0 // ======= // CONTROLLER_TOOLS_VERSION ?= v0.17.3 // >>>>>>> tmp-original-* Expect(makefileStr).To(ContainSubstring("<<<<<<<"), "Expected conflict marker <<<<<<< in Makefile") Expect(makefileStr).To(ContainSubstring("======="), "Expected conflict separator ======= in Makefile") Expect(makefileStr).To(ContainSubstring(">>>>>>>"), "Expected conflict marker >>>>>>> in Makefile") Expect(makefileStr).To(ContainSubstring("CONTROLLER_TOOLS_VERSION ?= v0.17.3"), "Expected original user version in conflict") Expect(makefileStr).To(ContainSubstring("CONTROLLER_TOOLS_VERSION ?= v0.18.0"), "Expected latest scaffold version in conflict") By("checking that the output branch (squashed) exists and is 1 commit ahead of main") prBranch := "kubebuilder-update-from-" + fromVersion + "-to-" + toVersionWithConflict git := func(args ...string) ([]byte, error) { cmd := exec.Command("git", args...) cmd.Dir = kbc.Dir return cmd.CombinedOutput() } By("checking that the squashed branch exists") _, err = git("rev-parse", "--verify", prBranch) Expect(err).NotTo(HaveOccurred()) By("checking that exactly one squashed commit ahead of main") count, err := git("rev-list", "--count", prBranch, "^main") Expect(err).NotTo(HaveOccurred(), string(count)) Expect(strings.TrimSpace(string(count))).To(Equal("1")) By("checking commit message of the squashed branch") msg, err := git("log", "-1", "--pretty=%B", prBranch) Expect(err).NotTo(HaveOccurred(), string(msg)) expected := helpers.ConflictCommitMessage(fromVersion, toVersionWithConflict) Expect(string(msg)).To(ContainSubstring(expected)) }) It("should stop when updating the project from v4.5.2 to v4.7.0 without the flag force", func() { By("running alpha update without --force flag") cmd := exec.Command( kbc.BinaryName, "alpha", "update", "--from-version", fromVersion, "--to-version", toVersionWithConflict, "--from-branch", "main", ) cmd.Dir = kbc.Dir out, err := kbc.Run(cmd) Expect(err).To(HaveOccurred()) Expect(string(out)).To(ContainSubstring("merge stopped due to conflicts")) By("validating that merge stopped with conflicts requiring manual resolution") validateConflictState(kbc.Dir) By("checking that custom code is preserved") validateCustomCodePreservation(kbc.Dir) By("checking that go module is upgraded") validateCommonGoModule(kbc.Dir) }) It("should preserve specified paths from base when squashing (e.g., .github/workflows)", func() { By("adding a workflow on main branch that should be preserved") wfDir := filepath.Join(kbc.Dir, ".github", "workflows") Expect(os.MkdirAll(wfDir, 0o755)).To(Succeed()) wf := filepath.Join(wfDir, "ci.yml") Expect(os.WriteFile(wf, []byte("name: KEEP_ME\n"), 0o644)).To(Succeed()) git := func(args ...string) { c := exec.Command("git", args...) c.Dir = kbc.Dir o, e := c.CombinedOutput() Expect(e).NotTo(HaveOccurred(), string(o)) } git("add", ".github/workflows/ci.yml") git("commit", "-m", "add ci workflow") By("running update (default squash) with --restore-path") cmd := exec.Command( kbc.BinaryName, "alpha", "update", "--from-version", fromVersion, "--to-version", toVersion, "--from-branch", "main", "--restore-path", ".github/workflows", ) cmd.Dir = kbc.Dir out, err := kbc.Run(cmd) Expect(err).NotTo(HaveOccurred(), string(out)) By("workflow content is preserved on output branch") data, err := os.ReadFile(wf) Expect(err).NotTo(HaveOccurred()) Expect(string(data)).To(ContainSubstring("KEEP_ME")) }) It("should succeed with no action when from-version and to-version are the same", func() { cmd := exec.Command(kbc.BinaryName, "alpha", "update", "--from-version", fromVersion, "--to-version", fromVersion, "--from-branch", "main") output, err := kbc.Run(cmd) Expect(err).NotTo(HaveOccurred()) Expect(string(output)).To(ContainSubstring("already uses the specified version")) Expect(string(output)).To(ContainSubstring("No action taken")) }) }) // Scaffolding (mock) is done with v4.11.1; the alpha update step is run with the current // binary (kbc.BinaryName from PATH, e.g. from make install) so we test current code changes. Context("alpha update with memcached operator and all plugins (Grafana, Helm, deploy-image)", func() { var ( pathBinV4111 string kbc *utils.TestContext ) BeforeEach(func() { var err error By("setting up test context (scaffold with v4.11.1; alpha update will use current binary)") kbc, err = utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on") Expect(err).NotTo(HaveOccurred()) Expect(kbc.Prepare()).To(Succeed()) By("downloading kubebuilder release " + memcachedFromVersion) pathBinV4111, err = downloadKubebuilderVersion(memcachedFromVersion) Expect(err).NotTo(HaveOccurred()) kb := pathBinV4111 dir := kbc.Dir By("initializing project (go/v4) as in generate.sh with-plugins") cmd := exec.Command(kb, "init", "--domain", "testproject.org", "--repo", "github.com/example/memcached-operator", "--plugins=go/v4") cmd.Dir = dir output, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("init failed: %s", output)) // Note: v4.11.1 does not support --namespaced on init/edit; we scaffold without it for compatibility By("creating Memcached API with deploy-image plugin (same args as generate.sh)") cmd = exec.Command(kb, "create", "api", "--group", "example.com", "--version", "v1alpha1", "--kind", "Memcached", "--image=memcached:1.6.26-alpine3.19", "--image-container-command=memcached,--memory-limit=64,-o,modern,-v", "--image-container-port=11211", "--run-as-user=1001", "--plugins=deploy-image/v1-alpha", "--make=false") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("create api Memcached failed: %s", output)) By("creating webhook for Memcached (programmatic-validation)") cmd = exec.Command(kb, "create", "webhook", "--group", "example.com", "--version", "v1alpha1", "--kind", "Memcached", "--programmatic-validation", "--make=false") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("create webhook Memcached failed: %s", output)) By("adding custom implementation to Memcached controller (uncomment Affinity block)") memcachedControllerPath := filepath.Join(dir, "internal", "controller", "memcached_controller.go") Expect(util.ReplaceRegexInFile(memcachedControllerPath, memcachedAffinityCommentedRegex, memcachedAffinityUncommented)). To(Succeed(), "failed to uncomment Affinity in memcached_controller.go") By("creating Busybox API with deploy-image plugin") cmd = exec.Command(kb, "create", "api", "--group", "example.com", "--version", "v1alpha1", "--kind", "Busybox", "--image=busybox:1.36.1", "--plugins=deploy-image/v1-alpha", "--make=false") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("create api Busybox failed: %s", output)) By("creating Wordpress v1 and v2 with conversion webhook") cmd = exec.Command(kb, "create", "api", "--group", "example.com", "--version", "v1", "--kind", "Wordpress", "--controller", "--resource", "--make=false") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("create api Wordpress v1 failed: %s", output)) cmd = exec.Command(kb, "create", "api", "--group", "example.com", "--version", "v2", "--kind", "Wordpress", "--controller=false", "--resource", "--make=false") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("create api Wordpress v2 failed: %s", output)) cmd = exec.Command(kb, "create", "webhook", "--group", "example.com", "--version", "v1", "--kind", "Wordpress", "--conversion", "--make=false", "--spoke", "v2") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("create webhook Wordpress conversion failed: %s", output)) By("editing project with Grafana plugin") cmd = exec.Command(kb, "edit", "--plugins=grafana.kubebuilder.io/v1-alpha") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("edit grafana failed: %s", output)) By("running make all") Expect(kbc.Make("all")).To(Succeed()) By("editing project with Helm plugin") cmd = exec.Command(kb, "edit", "--plugins=helm.kubebuilder.io/v2-alpha") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("edit helm failed: %s", output)) By("customizing Helm chart values (custom registry to be preserved after update)") valuesPath := filepath.Join(dir, "dist", "chart", "values.yaml") Expect(util.ReplaceInFile(valuesPath, "repository: controller", "repository: "+memcachedHelmCustomRegistry)). To(Succeed(), "failed to set custom registry in dist/chart/values.yaml") By("editing project with Auto Update plugin") cmd = exec.Command(kb, "edit", "--plugins=autoupdate.kubebuilder.io/v1-alpha", "--use-gh-models") cmd.Dir = dir output, err = cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("edit autoupdate failed: %s", output)) By("running go mod tidy") goTidy := exec.Command("go", "mod", "tidy") goTidy.Dir = dir output, err = goTidy.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("go mod tidy failed: %s", output)) By("initializing git and committing") initializeGitRepo(dir) }) AfterEach(func() { By("cleaning up test artifacts") if pathBinV4111 != "" { _ = os.RemoveAll(filepath.Dir(pathBinV4111)) } if kbc != nil { _ = os.RemoveAll(kbc.Dir) kbc.Destroy() } }) It("should update from v4.11.1 to current (latest) and regenerate Helm, Grafana, and deploy-image scaffolds", func() { // kbc.BinaryName is the current binary (from PATH / make install); we test current code. By("running alpha update from " + memcachedFromVersion + " to current (latest) with --force (temp dir: kbc.Dir)") cmd := exec.Command( kbc.BinaryName, "alpha", "update", "--from-version", memcachedFromVersion, "--from-branch", "main", "--force", ) cmd.Dir = kbc.Dir out, err := kbc.Run(cmd) Expect(err).NotTo(HaveOccurred(), string(out)) // With --force, update completes even with conflicts; result is on the output branch (squashed). projectDir := kbc.Dir By("checking that no unexpected conflict markers remain in key plugin outputs") // Allow conflicts only in Makefile or go.mod if versions changed; plugin scaffolds should be clean expectNoConflictMarkersInPath(projectDir, "dist/chart") expectNoConflictMarkersInPath(projectDir, "grafana") expectNoConflictMarkersInPath(projectDir, "api/v1alpha1") expectNoConflictMarkersInPath(projectDir, "internal/controller") expectNoConflictMarkersInPath(projectDir, "config/samples") expectNoConflictMarkersInPath(projectDir, "config/rbac") By("asserting Helm chart was properly regenerated (deploy-image resources included)") expectFileExists(projectDir, "dist/chart/Chart.yaml") expectFileExists(projectDir, "dist/chart/values.yaml") expectFileExists(projectDir, "dist/chart/templates/crd/memcacheds.example.com.testproject.org.yaml") expectFileExists(projectDir, "dist/chart/templates/crd/busyboxes.example.com.testproject.org.yaml") expectFileExists(projectDir, "dist/chart/templates/crd/wordpresses.example.com.testproject.org.yaml") expectFileExists(projectDir, "dist/chart/templates/rbac/memcached-admin-role.yaml") expectFileExists(projectDir, "dist/chart/templates/rbac/busybox-admin-role.yaml") By("asserting custom Helm values (registry) were preserved after regeneration") valuesContent, err := os.ReadFile(filepath.Join(projectDir, "dist/chart/values.yaml")) Expect(err).NotTo(HaveOccurred()) Expect(string(valuesContent)).To(ContainSubstring(memcachedHelmCustomRegistry), "custom registry in values.yaml must be preserved by alpha update") By("asserting Grafana dashboards were properly regenerated") expectFileExists(projectDir, "grafana/controller-runtime-metrics.json") expectFileExists(projectDir, "grafana/controller-resources-metrics.json") By("asserting deploy-image scaffolds (Memcached, Busybox) were properly regenerated") expectFileExists(projectDir, "api/v1alpha1/memcached_types.go") expectFileExists(projectDir, "api/v1alpha1/busybox_types.go") expectFileExists(projectDir, "internal/controller/memcached_controller.go") expectFileExists(projectDir, "internal/controller/busybox_controller.go") expectFileExists(projectDir, "config/samples/example.com_v1alpha1_memcached.yaml") expectFileExists(projectDir, "config/samples/example.com_v1alpha1_busybox.yaml") expectFileExists(projectDir, "config/rbac/memcached_admin_role.yaml") expectFileExists(projectDir, "config/rbac/busybox_admin_role.yaml") By("asserting custom Memcached controller implementation (uncommented Affinity) was preserved") memcachedControllerContent, err := os.ReadFile(filepath.Join(projectDir, "internal/controller/memcached_controller.go")) Expect(err).NotTo(HaveOccurred(), "failed to read memcached_controller.go") memcachedControllerStr := string(memcachedControllerContent) Expect(memcachedControllerStr).To(ContainSubstring("Affinity: &corev1.Affinity{"), "uncommented Affinity in memcached controller must be preserved by alpha update") Expect(memcachedControllerStr).To(ContainSubstring("NodeAffinity: &corev1.NodeAffinity{")) Expect(memcachedControllerStr).To(ContainSubstring(`Key: "kubernetes.io/arch"`), "node affinity MatchExpressions must be preserved") }) }) }) func modifyMakefileControllerTools(projectDir, newVersion string) { makefilePath := filepath.Join(projectDir, "Makefile") oldLine := "CONTROLLER_TOOLS_VERSION ?= v0.17.2" newLine := fmt.Sprintf("CONTROLLER_TOOLS_VERSION ?= %s", newVersion) By("replacing the controller-tools version in the Makefile") Expect(util.ReplaceInFile(makefilePath, oldLine, newLine)). To(Succeed(), "Failed to update CONTROLLER_TOOLS_VERSION in Makefile") By("committing the Makefile change to simulate user customization") cmds := [][]string{ {"git", "add", "Makefile"}, {"git", "commit", "-m", fmt.Sprintf("User modified CONTROLLER_TOOLS_VERSION to %s", newVersion)}, } for _, args := range cmds { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = projectDir output, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred(), fmt.Sprintf("Git command failed: %s", output)) } } func validateMakefileContent(projectDir string) { makefilePath := filepath.Join(projectDir, "Makefile") content, err := os.ReadFile(makefilePath) Expect(err).NotTo(HaveOccurred(), "Failed to read Makefile") makefile := string(content) Expect(makefile).To(ContainSubstring(`CONTROLLER_TOOLS_VERSION ?= v0.18.0`)) Expect(makefile).To(ContainSubstring(`GOLANGCI_LINT_VERSION ?= v2.1.0`)) Expect(makefile).To(ContainSubstring(`.PHONY: test-e2e`)) Expect(makefile).To(ContainSubstring(`go test ./test/e2e/ -v -ginkgo.v`)) Expect(makefile).To(ContainSubstring(`.PHONY: cleanup-test-e2e`)) Expect(makefile).To(ContainSubstring(`delete cluster --name $(KIND_CLUSTER)`)) } // 4.6.0 and 4.7.0 updates include common changes that should be validated func validateCommonGoModule(projectDir string) { expectModuleVersion(projectDir, "github.com/onsi/ginkgo/v2", "v2.22.0") expectModuleVersion(projectDir, "github.com/onsi/gomega", "v1.36.1") expectModuleVersion(projectDir, "k8s.io/apimachinery", "v0.33.0") expectModuleVersion(projectDir, "k8s.io/client-go", "v0.33.0") expectModuleVersion(projectDir, "sigs.k8s.io/controller-runtime", "") } func downloadKubebuilderVersion(version string) (string, error) { binaryDir, err := os.MkdirTemp("", "kubebuilder-"+version+"-") if err != nil { return "", fmt.Errorf("failed to create binary directory: %w", err) } url := fmt.Sprintf( "https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s", version, runtime.GOOS, runtime.GOARCH, ) binaryPath := filepath.Join(binaryDir, "kubebuilder") resp, err := http.Get(url) if err != nil { return "", fmt.Errorf("failed to download kubebuilder %s: %w", version, err) } defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to download kubebuilder %s: HTTP %d", version, resp.StatusCode) } file, err := os.Create(binaryPath) if err != nil { return "", fmt.Errorf("failed to create binary file: %w", err) } defer func() { _ = file.Close() }() _, err = io.Copy(file, resp.Body) if err != nil { return "", fmt.Errorf("failed to write binary: %w", err) } err = os.Chmod(binaryPath, 0o755) if err != nil { return "", fmt.Errorf("failed to make binary executable: %w", err) } return binaryPath, nil } func updateController(projectDir string) { controllerFile := filepath.Join(projectDir, "internal", "controller", "testoperator_controller.go") Expect(util.ReplaceInFile(controllerFile, "_ = logf.FromContext(ctx)", "log := logf.FromContext(ctx)")).To(Succeed()) Expect(util.ReplaceInFile(controllerFile, "// TODO(user): your logic here", controllerImplementation)).To(Succeed()) } func updateAPI(projectDir string) { typesFile := filepath.Join(projectDir, "api", "v1", "testoperator_types.go") err := util.ReplaceInFile(typesFile, "Foo string `json:\"foo,omitempty\"`", customField) Expect(err).NotTo(HaveOccurred(), "Failed to update testoperator_types.go") } func initializeGitRepo(projectDir string) { commands := [][]string{ {"git", "init"}, {"git", "config", "user.email", "test@example.com"}, {"git", "config", "user.name", "Test User"}, {"git", "add", "-A"}, {"git", "commit", "-m", "Initial project with custom code"}, {"git", "branch", "-M", "main"}, } for _, args := range commands { cmd := exec.Command(args[0], args[1:]...) cmd.Dir = projectDir _, err := cmd.CombinedOutput() if err != nil && strings.Contains(err.Error(), "already exists") { Expect(exec.Command("git", "checkout", "main").Run()).To(Succeed()) } else { Expect(err).NotTo(HaveOccurred()) } } } func validateCustomCodePreservation(projectDir string) { apiFile := filepath.Join(projectDir, "api", "v1", "testoperator_types.go") controllerFile := filepath.Join(projectDir, "internal", "controller", "testoperator_controller.go") apiContent, err := os.ReadFile(apiFile) Expect(err).NotTo(HaveOccurred()) Expect(string(apiContent)).To(ContainSubstring("Size int32 `json:\"size,omitempty\"`")) Expect(string(apiContent)).To(ContainSubstring("// +kubebuilder:validation:Minimum=0")) Expect(string(apiContent)).To(ContainSubstring("// +kubebuilder:validation:Maximum=3")) Expect(string(apiContent)).To(ContainSubstring("// +kubebuilder:default=1")) controllerContent, err := os.ReadFile(controllerFile) Expect(err).NotTo(HaveOccurred()) Expect(string(controllerContent)).To(ContainSubstring(controllerImplementation)) } func hasConflictMarkers(projectDir string) bool { hasMarker := false err := filepath.Walk(projectDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } content, readErr := os.ReadFile(path) if readErr != nil || bytes.Contains(content, []byte{0}) { return nil // skip unreadable or binary files } if strings.Contains(string(content), "<<<<<<<") { hasMarker = true return fmt.Errorf("conflict marker found in %s", path) // short-circuit early } return nil }) if err != nil && hasMarker { return true } return hasMarker } func validateConflictState(projectDir string) { By("validating merge stopped with conflicts requiring manual resolution") // 1. Check file contents for conflict markers Expect(hasConflictMarkers(projectDir)).To(BeTrue()) // 2. Check Git status for conflict-tracked files (UU = both modified) cmd := exec.Command("git", "status", "--porcelain") cmd.Dir = projectDir output, err := cmd.CombinedOutput() Expect(err).NotTo(HaveOccurred()) lines := strings.Split(string(output), "\n") conflictFound := false for _, line := range lines { if strings.HasPrefix(line, "UU ") || strings.HasPrefix(line, "AA ") { conflictFound = true break } } Expect(conflictFound).To(BeTrue(), "Expected Git to report conflict state in files") } func expectModuleVersion(projectDir, module, version string) { goModPath := filepath.Join(projectDir, "go.mod") content, err := os.ReadFile(goModPath) Expect(err).NotTo(HaveOccurred(), "Failed to read go.mod") expected := fmt.Sprintf("%s %s", module, version) Expect(string(content)).To(ContainSubstring(expected), fmt.Sprintf("Expected to find: %s", expected)) } // expectFileExists asserts that the given path under projectDir exists (file or dir). func expectFileExists(projectDir, relPath string) { p := filepath.Join(projectDir, relPath) _, err := os.Stat(p) Expect(err).NotTo(HaveOccurred(), "expected file or dir to exist: %s", p) } // expectNoConflictMarkersInPath asserts that no file under dir (relative to projectDir) contains Git conflict markers. // Skips if dir does not exist (e.g. optional plugin not scaffolded). func expectNoConflictMarkersInPath(projectDir, dir string) { root := filepath.Join(projectDir, dir) if _, err := os.Stat(root); os.IsNotExist(err) { return } _ = filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } content, readErr := os.ReadFile(path) if readErr != nil || bytes.Contains(content, []byte{0}) { return nil } Expect(string(content)).NotTo(ContainSubstring("<<<<<<<"), "expected no conflict markers in plugin output: %s", path) return nil }) } ================================================ FILE: internal/cli/alpha/internal/update/prepare.go ================================================ /* Copyright 2025 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 update import ( "encoding/json" "fmt" log "log/slog" "net/http" "strings" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/common" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" ) const defaultBranch = "main" type releaseResponse struct { TagName string `json:"tag_name"` } // Prepare resolves version and binary URL details after validation. // Should be called after Validate(). func (opts *Update) Prepare() error { if opts.FromBranch == "" { // TODO: Check if is possible to use get to determine the default branch log.Warn("No --from-branch specified, using 'main' as default") opts.FromBranch = defaultBranch } path, err := common.GetInputPath("") if err != nil { return fmt.Errorf("failed to determine project path: %w", err) } config, err := common.LoadProjectConfig(path) if err != nil { return fmt.Errorf("failed to load PROJECT config: %w", err) } opts.FromVersion, err = opts.defineFromVersion(config) if err != nil { return fmt.Errorf("failed to determine the version to use for the upgrade from: %w", err) } opts.ToVersion = opts.defineToVersion() return nil } // defineFromVersion will return the CLI version to be used for the update with the v prefix. func (opts *Update) defineFromVersion(config store.Store) (string, error) { fromVersion := opts.FromVersion if len(fromVersion) == 0 { fromVersion = config.Config().GetCliVersion() } if len(fromVersion) == 0 { return "", fmt.Errorf("no version specified in PROJECT file. " + "Please use --from-version flag to specify the version to update from") } if !strings.HasPrefix(fromVersion, "v") { fromVersion = "v" + fromVersion } return fromVersion, nil } func (opts *Update) defineToVersion() string { if len(opts.ToVersion) != 0 { if !strings.HasPrefix(opts.ToVersion, "v") { return "v" + opts.ToVersion } return opts.ToVersion } opts.ToVersion, _ = fetchLatestRelease() return opts.ToVersion } func fetchLatestRelease() (string, error) { resp, err := http.Get("https://api.github.com/repos/kubernetes-sigs/kubebuilder/releases/latest") if err != nil { return "", fmt.Errorf("failed to fetch latest release: %w", err) } defer func() { if err := resp.Body.Close(); err != nil { log.Info("failed to close connection", "error", err) } }() if resp.StatusCode != http.StatusOK { return "", fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var release releaseResponse if err := json.NewDecoder(resp.Body).Decode(&release); err != nil { return "", fmt.Errorf("failed to parse response: %w", err) } return release.TagName, nil } ================================================ FILE: internal/cli/alpha/internal/update/prepare_test.go ================================================ //go:build integration /* Copyright 2025 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 update import ( "fmt" "os" "path/filepath" "github.com/h2non/gock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/common" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" v3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" ) const ( testFromVersion = "v4.5.0" testToVersion = "v4.6.0" ) var _ = Describe("Prepare for internal update", func() { var ( tmpDir string workDir string projectFile string mockGh string err error logFile string oldPath string opts Update ) BeforeEach(func() { workDir, err = os.Getwd() Expect(err).ToNot(HaveOccurred()) // 1) Create tmp dir and chdir first tmpDir, err = os.MkdirTemp("", "kubebuilder-prepare-test") Expect(err).ToNot(HaveOccurred()) err = os.Chdir(tmpDir) Expect(err).ToNot(HaveOccurred()) // 2) Now that tmpDir exists, set logFile and PATH logFile = filepath.Join(tmpDir, "bin.log") oldPath = os.Getenv("PATH") Expect(os.Setenv("PATH", tmpDir+string(os.PathListSeparator)+oldPath)).To(Succeed()) projectFile = filepath.Join(tmpDir, yaml.DefaultPath) config.Register(config.Version{Number: 3}, func() config.Config { return &v3.Cfg{Version: config.Version{Number: 3}, CliVersion: "v1.0.0"} }) gock.New("https://api.github.com"). Get("/repos/kubernetes-sigs/kubebuilder/releases/latest"). Reply(200). JSON(map[string]string{"tag_name": "v1.1.0"}) // 3) Create the mock gh inside tmpDir (on PATH) mockGh = filepath.Join(tmpDir, "gh") ghOK := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "repo" && "$2" == "view" ]]; then echo "acme/repo" exit 0 fi if [[ "$1" == "issue" && "$2" == "create" ]]; then exit 0 fi exit 0` Expect(mockBinResponse(ghOK, mockGh)).To(Succeed()) }) AfterEach(func() { Expect(os.Setenv("PATH", oldPath)).To(Succeed()) err = os.Chdir(workDir) Expect(err).ToNot(HaveOccurred()) err = os.RemoveAll(tmpDir) Expect(err).ToNot(HaveOccurred()) defer gock.Off() }) Context("Prepare", func() { DescribeTable("should succeed for valid options", func(options *Update) { const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) result := options.Prepare() Expect(result).ToNot(HaveOccurred()) Expect(options.Prepare()).To(Succeed()) Expect(options.FromVersion).To(Equal("v1.0.0")) Expect(options.ToVersion).To(Equal("v1.1.0")) }, Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test"}), Entry("options", &Update{FromVersion: "1.0.0", ToVersion: "1.1.0", FromBranch: "test"}), Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0"}), Entry("options", &Update{}), ) DescribeTable("Should fail to prepare if project path is undetermined", func(options *Update) { err = options.Prepare() Expect(err).To(HaveOccurred()) Expect(err.Error()).Should(ContainSubstring("failed to determine project path")) }, Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test"}), ) DescribeTable("Should fail if PROJECT config could not be loaded", func(options *Update) { const version = "" Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) err = options.Prepare() Expect(err).To(HaveOccurred()) Expect(err.Error()).Should(ContainSubstring("failed to load PROJECT config")) }, Entry("options", &Update{FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test"}), ) DescribeTable("Should fail if FromVersion cannot be determined", func(options *Update) { config.Register(config.Version{Number: 3}, func() config.Config { return &v3.Cfg{Version: config.Version{Number: 3}} }) const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) Expect(options.FromVersion).To(BeEquivalentTo("")) }, Entry("options", &Update{}), ) }) Context("DefineFromVersion", func() { DescribeTable("Should succeed when --from-version or CliVersion in Project config is present", func(options *Update) { const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) config, errLoad := common.LoadProjectConfig(tmpDir) Expect(errLoad).ToNot(HaveOccurred()) fromVersion, errLoad := options.defineFromVersion(config) Expect(errLoad).ToNot(HaveOccurred()) Expect(fromVersion).To(BeEquivalentTo("v1.0.0")) }, Entry("options", &Update{FromVersion: ""}), Entry("options", &Update{FromVersion: "1.0.0"}), ) DescribeTable("Should fail when --from-version and CliVersion in Project config both are absent", func(options *Update) { config.Register(config.Version{Number: 3}, func() config.Config { return &v3.Cfg{Version: config.Version{Number: 3}} }) const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) config, errLoad := common.LoadProjectConfig(tmpDir) Expect(errLoad).NotTo(HaveOccurred()) fromVersion, errLoad := options.defineFromVersion(config) Expect(errLoad).To(HaveOccurred()) Expect(errLoad.Error()).To(ContainSubstring("no version specified in PROJECT file")) Expect(fromVersion).To(Equal("")) }, Entry("options", &Update{FromVersion: ""}), ) }) Context("DefineToVersion", func() { DescribeTable("Should succeed.", func(options *Update) { toVersion := options.defineToVersion() Expect(toVersion).To(BeEquivalentTo("v1.1.0")) }, Entry("options", &Update{ToVersion: "1.1.0"}), Entry("options", &Update{ToVersion: "v1.1.0"}), Entry("options", &Update{}), ) }) Context("OpenGitHubIssue", func() { It("creates issue without conflicts", func() { opts.FromBranch = defaultBranch opts.FromVersion = "v4.5.1" opts.ToVersion = "v4.8.0" err = opts.openGitHubIssue(false) Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) s := string(logs) Expect(s).To(ContainSubstring("repo view --json nameWithOwner --jq .nameWithOwner")) Expect(s).To(ContainSubstring("issue create")) expURL := fmt.Sprintf("https://github.com/%s/compare/%s...%s?expand=1", "acme/repo", opts.FromBranch, opts.getOutputBranchName()) Expect(s).To(ContainSubstring(expURL)) Expect(s).To(ContainSubstring(opts.ToVersion)) Expect(s).To(ContainSubstring(opts.FromVersion)) }) It("creates issue with conflicts template", func() { opts.FromBranch = defaultBranch opts.FromVersion = "v4.5.2" opts.ToVersion = "v4.10.0" err = opts.openGitHubIssue(true) Expect(err).ToNot(HaveOccurred()) logs, _ := os.ReadFile(logFile) s := string(logs) Expect(s).To(ContainSubstring("Resolve conflicts")) Expect(s).To(ContainSubstring("make manifests generate fmt vet lint-fix")) }) It("fails when repo detection fails", func() { failRepo := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "repo" && "$2" == "view" ]]; then exit 1 fi exit 0` Expect(mockBinResponse(failRepo, mockGh)).To(Succeed()) err = opts.openGitHubIssue(false) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to detect GitHub repository")) }) It("fails when issue creation fails", func() { failIssue := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "repo" && "$2" == "view" ]]; then echo "acme/repo" exit 0 fi if [[ "$1" == "issue" && "$2" == "create" ]]; then exit 1 fi exit 0` Expect(mockBinResponse(failIssue, mockGh)).To(Succeed()) opts.FromBranch = defaultBranch opts.FromVersion = testFromVersion opts.ToVersion = testToVersion err = opts.openGitHubIssue(false) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to create GitHub issue: exit status 1")) }) }) Context("Version Handling Edge Cases", func() { DescribeTable("Should handle version prefix normalization in defineToVersion", func(inputVersion, expectedVersion string) { opts := &Update{ToVersion: inputVersion} normalizedVersion := opts.defineToVersion() Expect(normalizedVersion).To(Equal(expectedVersion)) }, Entry("adds v prefix when missing", "1.0.0", "v1.0.0"), Entry("keeps v prefix when present", "v1.0.0", "v1.0.0"), Entry("handles semantic versioning", "1.2.3", "v1.2.3"), Entry("handles pre-release versions", "1.0.0-alpha", "v1.0.0-alpha"), Entry("handles build metadata", "1.0.0+build.1", "v1.0.0+build.1"), ) DescribeTable("Should handle malformed versions gracefully during validation", func(invalidFromVersion, invalidToVersion string) { const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) opts := &Update{FromVersion: invalidFromVersion, ToVersion: invalidToVersion, FromBranch: "test"} err = opts.Prepare() // Should handle gracefully or provide clear error message if err != nil { Expect(err.Error()).To(Or( ContainSubstring("version"), ContainSubstring("validate"), ContainSubstring("semantic"), )) } }, Entry("invalid from version", "not.a.version", "v1.0.0"), Entry("invalid to version", "v1.0.0", "not.a.version"), Entry("special characters in from", "v1.0.0$invalid", "v1.0.0"), Entry("special characters in to", "v1.0.0", "v1.0.0$invalid"), ) }) Context("GitHub Integration Edge Cases", func() { BeforeEach(func() { opts.FromBranch = defaultBranch opts.FromVersion = testFromVersion opts.ToVersion = testToVersion }) It("handles missing gh CLI", func() { noGh := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "repo" && "$2" == "view" ]]; then echo "command not found: gh" >&2 exit 127 fi exit 0` Expect(mockBinResponse(noGh, mockGh)).To(Succeed()) err = opts.openGitHubIssue(false) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to detect GitHub repository")) }) It("handles gh CLI authentication failure", func() { authFailGh := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "repo" && "$2" == "view" ]]; then echo "error: authentication required" >&2 exit 1 fi exit 0` Expect(mockBinResponse(authFailGh, mockGh)).To(Succeed()) err = opts.openGitHubIssue(false) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to detect GitHub repository")) }) }) Context("Output Branch Name Generation", func() { DescribeTable("Should generate correct output branch names", func(fromVersion, toVersion, expectedSuffix string) { opts := &Update{ FromVersion: fromVersion, ToVersion: toVersion, } branchName := opts.getOutputBranchName() Expect(branchName).To(ContainSubstring("kubebuilder-update-from")) Expect(branchName).To(ContainSubstring(expectedSuffix)) }, Entry("standard versions", "v1.0.0", "v1.1.0", "v1.0.0-to-v1.1.0"), Entry("versions without v prefix", "1.0.0", "1.1.0", "1.0.0-to-1.1.0"), Entry("pre-release versions", "v1.0.0-alpha", "v1.1.0-beta", "v1.0.0-alpha-to-v1.1.0-beta"), ) It("uses custom output branch when specified", func() { customBranch := "my-custom-update-branch" opts := &Update{ FromVersion: "v1.0.0", ToVersion: "v1.1.0", OutputBranch: customBranch, } branchName := opts.getOutputBranchName() Expect(branchName).To(Equal(customBranch)) }) }) Context("Git Configuration Validation", func() { It("should handle empty git config", func() { opts := &Update{ FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test", GitConfig: []string{}, // Empty git config } const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) err = opts.Prepare() Expect(err).ToNot(HaveOccurred()) }) It("should handle invalid git config format", func() { opts := &Update{ FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test", GitConfig: []string{"invalid-config-format"}, // Invalid format } const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) err = opts.Prepare() Expect(err).ToNot(HaveOccurred()) }) }) Context("Branch Name Validation", func() { DescribeTable("Should handle various branch name formats", func(branchName string, shouldSucceed bool) { opts := &Update{ FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: branchName, } const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) err = opts.Prepare() if shouldSucceed { Expect(err).ToNot(HaveOccurred()) } else { Expect(err).To(HaveOccurred()) } }, Entry("standard main branch", "main", true), Entry("standard master branch", "master", true), Entry("feature branch", "feature/my-feature", true), Entry("release branch", "release/v1.0.0", true), Entry("branch with numbers", "branch-123", true), Entry("empty branch name", "", true), ) }) Context("Resource Cleanup and Error Recovery", func() { It("should handle cleanup when preparation fails", func() { // This test ensures that temporary resources are cleaned up even when operations fail failGit := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` mockGit := filepath.Join(tmpDir, "git") Expect(mockBinResponse(failGit, mockGit)).To(Succeed()) opts := &Update{ FromVersion: "v1.0.0", ToVersion: "v1.1.0", FromBranch: "test", } const version = `version: "3"` Expect(os.WriteFile(projectFile, []byte(version), 0o644)).To(Succeed()) err = opts.Prepare() // The specific error depends on when git fails in the preparation process // This test ensures the system handles git failures gracefully if err != nil { Expect(err.Error()).To(ContainSubstring("git")) } }) }) }) ================================================ FILE: internal/cli/alpha/internal/update/suite_test.go ================================================ /* Copyright 2025 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 update import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestCommand(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "alpha command: update suite") } ================================================ FILE: internal/cli/alpha/internal/update/update.go ================================================ /* Copyright 2025 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 update import ( "bufio" "bytes" "errors" "fmt" "io" log "log/slog" "os" "os/exec" "regexp" "strings" "time" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/update/helpers" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" ) // Update contains configuration for the update operation. type Update struct { // FromVersion is the release version to update FROM (the base/original scaffold), // e.g., "v4.5.0". This is used to regenerate the ancestor scaffold. FromVersion string // ToVersion is the release version to update TO (the target scaffold), // e.g., "v4.6.0". This is used to regenerate the upgrade scaffold. ToVersion string // FromBranch is the base Git branch that represents the user's current project state, // e.g., "main". Its contents are captured into the "original" branch during the update. FromBranch string // Force, when true, commits the merge result even if there are conflicts. // In that case, conflict markers are kept in the files. Force bool // ShowCommits controls whether to keep full history (no squash). // - true => keep history: point the output branch at the merge commit // (no squashed commit is created). // - false => squash: write the merge tree as a single commit on the output branch. // // The output branch name defaults to "kubebuilder-update-from--to-" // unless OutputBranch is explicitly set. ShowCommits bool // RestorePath is a list of paths to restore from the base branch (FromBranch) // when SQUASHING, so things like CI config remain unchanged. // Example: []string{".github/workflows"} // NOTE: This is ignored when ShowCommits == true. RestorePath []string // OutputBranch is the name of the branch that will receive the result: // - In squash mode (ShowCommits == false): the single squashed commit. // - In keep-history mode (ShowCommits == true): the merge commit. // If empty, it defaults to "kubebuilder-update-from--to-". OutputBranch string // Push, when true, pushes the OutputBranch to the "origin" remote after the update completes. Push bool // CommitMessage is the custom merge message to use for successful merges (no conflicts). // Set via --merge-message flag. // If empty, defaults to: "chore(kubebuilder): update scaffold -> ". CommitMessage string // CommitMessageConflict is the custom conflict message to use when conflicts occur. // Set via --conflict-message flag. // If empty, defaults to: "chore(kubebuilder): (:warning: manual conflict resolution required) // update scaffold -> ". CommitMessageConflict string // OpenGhIssue, when true, automatically creates a GitHub issue after the update // completes. The issue includes a pre-filled checklist and a compare link from // the base branch (--from-branch) to the output branch. This requires the GitHub // CLI (`gh`) to be installed and authenticated in the local environment. OpenGhIssue bool UseGhModels bool // GitConfig holds per-invocation Git settings applied to every `git` command via // `git -c key=value`. // // Examples: // []string{"merge.renameLimit=999999"} // improve rename detection during merges // []string{"diff.renameLimit=999999"} // improve rename detection during diffs // []string{"merge.conflictStyle=diff3"} // show ancestor in conflict markers // []string{"rerere.enabled=true"} // reuse recorded resolutions // // Defaults: // When no --git-config flags are provided, the updater adds: // []string{"merge.renameLimit=999999", "diff.renameLimit=999999"} // // Behavior: // • If one or more --git-config flags are supplied, those values are appended on top of the defaults. // • To disable the defaults entirely, include a literal "disable", for example: // --git-config disable --git-config rerere.enabled=true GitConfig []string // Temporary branches created during the update process. These are internal to the run // and are surfaced for transparency/debugging: // - AncestorBranch: clean scaffold generated from FromVersion // - OriginalBranch: snapshot of the user's current project (FromBranch) // - UpgradeBranch: clean scaffold generated from ToVersion // - MergeBranch: result of merging Original into Upgrade (pre-output) AncestorBranch string OriginalBranch string UpgradeBranch string MergeBranch string } // Update a project using a default three-way Git merge. // This helps apply new scaffolding changes while preserving custom code. func (opts *Update) Update() error { // Inform users about GitHub Models if they're opening an issue but not using AI summary if opts.OpenGhIssue && !opts.UseGhModels { log.Info("Consider enabling GitHub Models to get an AI summary to help with the update") log.Info("Use the --use-gh-models flag if your project/organization has permission to use GitHub Models") } log.Info("Checking out base branch", "branch", opts.FromBranch) checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch) if err := checkoutCmd.Run(); err != nil { return fmt.Errorf("failed to checkout base branch %s: %w", opts.FromBranch, err) } suffix := time.Now().Format("02-01-06-15-04") opts.AncestorBranch = "tmp-ancestor-" + suffix opts.OriginalBranch = "tmp-original-" + suffix opts.UpgradeBranch = "tmp-upgrade-" + suffix opts.MergeBranch = "tmp-merge-" + suffix log.Debug("temporary branches", "ancestor", opts.AncestorBranch, "original", opts.OriginalBranch, "upgrade", opts.UpgradeBranch, "merge", opts.MergeBranch, ) // 1. Creates an ancestor branch based on base branch // 2. Deletes everything except .git and PROJECT // 3. Installs old release // 4. Runs alpha generate with old release binary // 5. Commits the result log.Info("Preparing Ancestor branch", "branch_name", opts.AncestorBranch) if err := opts.prepareAncestorBranch(); err != nil { return fmt.Errorf("failed to prepare ancestor branch: %w", err) } // 1. Creates original branch // 2. Ensure that original branch is == Based on user’s current base branch content with // git checkout "main" -- . // 3. Commits this state log.Info("Preparing Original branch", "branch_name", opts.OriginalBranch) if err := opts.prepareOriginalBranch(); err != nil { return fmt.Errorf("failed to checkout current off ancestor: %w", err) } // 1. Creates upgrade branch from ancestor // 2. Cleans up the branch by removing all files except .git and PROJECT // 2. Regenerates scaffold using alpha generate with new version // 3. Commits the result log.Info("Preparing Upgrade branch", "branch_name", opts.UpgradeBranch) if err := opts.prepareUpgradeBranch(); err != nil { return fmt.Errorf("failed to checkout upgrade off ancestor: %w", err) } // 1. Creates merge branch from upgrade // 2. Merges in original (user code) // 3. If conflicts occur, it will warn the user and leave the merge branch for manual resolution // 4. If merge is clean, it stages the changes and commits the result log.Info("Preparing Merge branch and performing merge", "branch_name", opts.MergeBranch) hasConflicts, err := opts.mergeOriginalToUpgrade() if err != nil { return fmt.Errorf("failed to merge upgrade into merge branch: %w", err) } // Squash or keep commits based on ShowCommits flag if opts.ShowCommits { log.Info("Keeping commits history") out := opts.getOutputBranchName() if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", out, opts.MergeBranch).Run(); err != nil { return fmt.Errorf("checkout %s: %w", out, err) } } else { log.Info("Squashing merge result to output branch", "output_branch", opts.getOutputBranchName()) if err := opts.squashToOutputBranch(hasConflicts); err != nil { return fmt.Errorf("failed to squash to output branch: %w", err) } } // Push the output branch if requested if opts.Push { if opts.Push { out := opts.getOutputBranchName() _ = helpers.GitCmd(opts.GitConfig, "checkout", out).Run() if err := helpers.GitCmd(opts.GitConfig, "push", "-u", "origin", out).Run(); err != nil { return fmt.Errorf("failed to push %s: %w", out, err) } } } opts.cleanupTempBranches() log.Info("Update completed successfully") if opts.OpenGhIssue { if err := opts.openGitHubIssue(hasConflicts); err != nil { return fmt.Errorf("failed to open GitHub issue: %w", err) } } return nil } func (opts *Update) openGitHubIssue(hasConflicts bool) error { log.Info("Creating GitHub Issue to track the need to update the project") out := opts.getOutputBranchName() // Detect repo "owner/name" repoCmd := exec.Command("gh", "repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner") repoBytes, err := repoCmd.Output() if err != nil { return fmt.Errorf("failed to detect GitHub repository via `gh repo view`: %s", err) } repo := strings.TrimSpace(string(repoBytes)) createPRURL := fmt.Sprintf("https://github.com/%s/compare/%s...%s?expand=1", repo, opts.FromBranch, out) title := fmt.Sprintf(helpers.IssueTitleTmpl, opts.ToVersion, opts.FromVersion) // Skip if an open issue with same title already exists checkCmd := exec.Command("gh", "issue", "list", "--repo", repo, "--state", "open", "--search", fmt.Sprintf("in:title \"%s\"", title), "--json", "title") if checkOut, checkErr := checkCmd.Output(); checkErr == nil && strings.Contains(string(checkOut), title) { log.Info("GitHub Issue already exists, skipping creation", "title", title) return nil } // Base issue body var body string if hasConflicts { body = fmt.Sprintf(helpers.IssueBodyTmplWithConflicts, opts.ToVersion, createPRURL, opts.FromVersion, out) } else { body = fmt.Sprintf(helpers.IssueBodyTmpl, opts.ToVersion, createPRURL, opts.FromVersion, out) } log.Info("Creating GitHub Issue") createCmd := exec.Command("gh", "issue", "create", "--repo", repo, "--title", title, "--body", body, ) createOut, createErr := createCmd.CombinedOutput() if createErr != nil { return fmt.Errorf("failed to create GitHub issue: %v\n%s", createErr, string(createOut)) } outStr := string(createOut) // Try to extract the issue URL from stdout issueURL := helpers.FirstURL(outStr) // Fallback: query the just-created issue by title if issueURL == "" { viewCmd := exec.Command("gh", "issue", "list", "--repo", repo, "--state", "open", "--search", fmt.Sprintf("in:title \"%s\"", title), "--json", "url", "--jq", ".[0].url", ) urlBytes, vErr := viewCmd.Output() if vErr != nil { log.Warn("could not determine issue URL from gh output", "stdout", outStr, "error", vErr) } issueURL = strings.TrimSpace(string(urlBytes)) } log.Info("GitHub Issue created to track the update", "url", issueURL, "compare", createPRURL) if opts.UseGhModels { log.Info("Generating AI summary with gh models") if issueURL == "" { return fmt.Errorf("issue created but URL could not be determined") } releaseURL := fmt.Sprintf("https://github.com/kubernetes-sigs/kubebuilder/releases/tag/%s", opts.ToVersion) ctx := helpers.BuildFullPrompet( opts.FromVersion, opts.ToVersion, opts.FromBranch, out, createPRURL, releaseURL) var outBuf, errBuf bytes.Buffer cmd := exec.Command( "gh", "models", "run", "openai/gpt-5", "--system-prompt", helpers.AiPRPrompt, ) cmd.Stdin = strings.NewReader(ctx) cmd.Stdout = &outBuf cmd.Stderr = &errBuf if err := cmd.Run(); err != nil { return fmt.Errorf("gh models run failed: %w\nstderr:\n%s", err, errBuf.String()) } summary := strings.TrimSpace(outBuf.String()) if summary != "" { num := helpers.IssueNumberFromURL(issueURL) target := issueURL args := make([]string, 4, 7) args[0] = "issue" args[1] = "comment" args[2] = "--repo" args[3] = repo if num != "" { target = num } args = append(args, target, "--body", summary) commentCmd := exec.Command("gh", args...) commentCmd.Stdout = os.Stdout commentCmd.Stderr = os.Stderr if err := commentCmd.Run(); err != nil { return fmt.Errorf("failed to add AI summary comment: %s", err) } log.Info("AI summary comment added to the issue") } else { log.Warn("AI summary was empty, no comment added") } } return nil } func (opts *Update) cleanupTempBranches() { _ = helpers.GitCmd(opts.GitConfig, "checkout", opts.getOutputBranchName()).Run() branches := []string{ opts.AncestorBranch, opts.OriginalBranch, opts.UpgradeBranch, opts.MergeBranch, } for _, b := range branches { b = strings.TrimSpace(b) if b == "" { continue } // Delete only if it's a LOCAL branch. if err := helpers.GitCmd(opts.GitConfig, "show-ref", "--verify", "--quiet", "refs/heads/"+b).Run(); err == nil { _ = helpers.GitCmd(opts.GitConfig, "branch", "-D", b).Run() } } } // getOutputBranchName returns the output branch name func (opts *Update) getOutputBranchName() string { if opts.OutputBranch != "" { return opts.OutputBranch } return fmt.Sprintf("kubebuilder-update-from-%s-to-%s", opts.FromVersion, opts.ToVersion) } // preservePaths checks out the paths specified in RestorePath func (opts *Update) preservePaths() { for _, p := range opts.RestorePath { p = strings.TrimSpace(p) if p == "" { continue } if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", p).Run(); err != nil { log.Warn("failed to restore preserved path", "path", p, "branch", opts.FromBranch, "error", err) } } } // squashToOutputBranch takes the exact tree of the MergeBranch and writes it as ONE commit // on a branch derived from FromBranch (e.g., "main"). If RestorePath is set, those paths // are restored from the base branch after copying the merge tree, so CI config etc. stays put. func (opts *Update) squashToOutputBranch(hasConflicts bool) error { out := opts.getOutputBranchName() // 1) base -> out if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch).Run(); err != nil { return fmt.Errorf("checkout %s: %w", opts.FromBranch, err) } if err := helpers.GitCmd(opts.GitConfig, "checkout", "-B", out, opts.FromBranch).Run(); err != nil { return fmt.Errorf("create/reset %s from %s: %w", out, opts.FromBranch, err) } // 2) clean worktree, then copy merge tree if err := helpers.CleanWorktree("output branch"); err != nil { return fmt.Errorf("output branch: %w", err) } if err := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch, "--", ".").Run(); err != nil { return fmt.Errorf("checkout %s content: %w", "merge", err) } // 3) optionally restore preserved paths from base (tests assert on 'git restore …') opts.preservePaths() // 4) stage and single squashed commit if err := helpers.GitCmd(opts.GitConfig, "add", "--all").Run(); err != nil { return fmt.Errorf("stage output: %w", err) } if err := helpers.CommitIgnoreEmpty(opts.getMergeMessage(hasConflicts), "final"); err != nil { return fmt.Errorf("failed to commit final branch: %w", err) } return nil } // regenerateProjectWithVersion downloads the release binary for the specified version, // and runs the `alpha generate` command to re-scaffold the project func regenerateProjectWithVersion(version string) error { tempDir, err := helpers.DownloadReleaseVersionWith(version) if err != nil { return fmt.Errorf("failed to download release %s binary: %w", version, err) } if err := runAlphaGenerate(tempDir, version); err != nil { return fmt.Errorf("failed to run alpha generate on ancestor branch: %w", err) } return nil } // prepareAncestorBranch prepares the ancestor branch by checking it out, // cleaning up the project files, and regenerating the project with the specified version. func (opts *Update) prepareAncestorBranch() error { if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.AncestorBranch, opts.FromBranch).Run(); err != nil { return fmt.Errorf("failed to create %s from %s: %w", opts.AncestorBranch, opts.FromBranch, err) } if err := cleanupBranch(); err != nil { return fmt.Errorf("failed to cleanup the %s : %w", opts.AncestorBranch, err) } if err := regenerateProjectWithVersion(opts.FromVersion); err != nil { return fmt.Errorf("failed to regenerate project with fromVersion %s: %w", opts.FromVersion, err) } gitCmd := helpers.GitCmd(opts.GitConfig, "add", "--all") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes in %s: %w", opts.AncestorBranch, err) } commitMessage := "(chore) initial scaffold from release version: " + opts.FromVersion if err := helpers.CommitIgnoreEmpty(commitMessage, "ancestor"); err != nil { return fmt.Errorf("failed to commit ancestor branch: %w", err) } return nil } // cleanupBranch removes all files and folders in the current directory // except for the .git directory and the PROJECT file. // This is necessary to ensure the ancestor branch starts with a clean slate // TODO: Analise if this command is still needed in the future. // It is required because the alpha generate command in versions prior to v4.7.0 do not properly // handle the removal of files in the ancestor branch. func cleanupBranch() error { cmd := exec.Command("sh", "-c", "find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {} +") if err := cmd.Run(); err != nil { return fmt.Errorf("failed to clean up files: %w", err) } return nil } // runMakeTargets runs the make targets needed to keep the tree consistent. // If skipConflicts is true, it avoids running targets that are guaranteed // to fail noisily when there are unresolved conflicts. func runMakeTargets(skipConflicts bool) { if !skipConflicts { for _, t := range []string{"manifests", "generate", "fmt", "vet", "lint-fix"} { if err := util.RunCmd(fmt.Sprintf("Running make %s", t), "make", t); err != nil { log.Warn("make target failed", "target", t, "error", err) } } return } // Conflict-aware path: decide what to run based on repo state. cs := helpers.DetectConflicts() targets := helpers.DecideMakeTargets(cs) if cs.Makefile { log.Warn("Skipping all make targets because Makefile has merge conflicts") return } if cs.API { log.Warn("API conflicts detected; skipping make targets: manifests, generate") } if cs.AnyGo { log.Warn("Go conflicts detected; skipping make targets: fmt, vet, lint-fix") } if len(targets) == 0 { log.Warn("No make targets will be run due to conflicts") return } for _, t := range targets { if err := util.RunCmd(fmt.Sprintf("Running make %s", t), "make", t); err != nil { log.Warn("make target failed", "target", t, "error", err) } } } // runAlphaGenerate executes the old Kubebuilder version's 'alpha generate' command // to create clean scaffolding in the ancestor branch. This uses the downloaded // binary with the original PROJECT file to recreate the project's initial state. func runAlphaGenerate(tempDir, version string) error { log.Info("Generating project", "version", version) tempBinaryPath := tempDir + "/kubebuilder" cmd := exec.Command(tempBinaryPath, "alpha", "generate") cmd.Env = envWithPrefixedPath(tempDir) // Capture and reformat subprocess output to match our logging style stdout, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("failed to create stdout pipe: %w", err) } stderr, err := cmd.StderrPipe() if err != nil { return fmt.Errorf("failed to create stderr pipe: %w", err) } if err := cmd.Start(); err != nil { return fmt.Errorf("failed to start alpha generate: %w", err) } // Forward output while reformatting old-style logs go forwardAndReformat(stdout, false) go forwardAndReformat(stderr, true) if err := cmd.Wait(); err != nil { return fmt.Errorf("failed to run alpha generate: %w", err) } log.Info("Project scaffold generation complete", "version", version) runMakeTargets(false) return nil } // forwardAndReformat reads from a subprocess stream and reformats old-style logging to new style func forwardAndReformat(reader io.Reader, isStderr bool) { scanner := bufio.NewScanner(reader) // Regex to match old-style log format: level=info msg="message" logPattern := regexp.MustCompile(`^level=(\w+)\s+msg="?([^"]*)"?(.*)$`) for scanner.Scan() { line := scanner.Text() // Check if this line matches the old log format if matches := logPattern.FindStringSubmatch(line); matches != nil { level := strings.ToUpper(matches[1]) message := matches[2] rest := matches[3] // Convert to new format based on level switch level { case "INFO": log.Info(message + rest) case "WARN", "WARNING": log.Warn(message + rest) case "ERROR": log.Error(message + rest) case "DEBUG": log.Debug(message + rest) default: // Fallback: print as-is to appropriate stream if isStderr { fmt.Fprintln(os.Stderr, line) } else { fmt.Println(line) } } } else { // Not a log line, print as-is to appropriate stream if isStderr { fmt.Fprintln(os.Stderr, line) } else { fmt.Println(line) } } } } func envWithPrefixedPath(dir string) []string { env := os.Environ() prefix := "PATH=" for i, kv := range env { if after, ok := strings.CutPrefix(kv, prefix); ok { env[i] = "PATH=" + dir + string(os.PathListSeparator) + after return env } } return append(env, "PATH="+dir) } // prepareOriginalBranch creates the 'original' branch from ancestor and // populates it with the user's actual project content from the default branch. // This represents the current state of the user's project. func (opts *Update) prepareOriginalBranch() error { gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.OriginalBranch) if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to checkout branch %s: %w", opts.OriginalBranch, err) } gitCmd = helpers.GitCmd(opts.GitConfig, "checkout", opts.FromBranch, "--", ".") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to checkout content from %s branch onto %s: %w", opts.FromBranch, opts.OriginalBranch, err) } gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage all changes in current: %w", err) } if err := helpers.CommitIgnoreEmpty( fmt.Sprintf("(chore) original code from %s to keep changes", opts.FromBranch), "original", ); err != nil { return fmt.Errorf("failed to commit original branch: %w", err) } return nil } // prepareUpgradeBranch creates the 'upgrade' branch from ancestor and // generates fresh scaffolding using the current (latest) CLI version. // This represents what the project should look like with the new version. func (opts *Update) prepareUpgradeBranch() error { gitCmd := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.UpgradeBranch, opts.AncestorBranch) if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to checkout %s branch off %s: %w", opts.UpgradeBranch, opts.AncestorBranch, err) } checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.UpgradeBranch) if err := checkoutCmd.Run(); err != nil { return fmt.Errorf("failed to checkout base branch %s: %w", opts.UpgradeBranch, err) } if err := cleanupBranch(); err != nil { return fmt.Errorf("failed to cleanup the %s branch: %w", opts.UpgradeBranch, err) } if err := regenerateProjectWithVersion(opts.ToVersion); err != nil { return fmt.Errorf("failed to regenerate project with version %s: %w", opts.ToVersion, err) } gitCmd = helpers.GitCmd(opts.GitConfig, "add", "--all") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes in %s: %w", opts.UpgradeBranch, err) } if err := helpers.CommitIgnoreEmpty( "(chore) initial scaffold from release version: "+opts.ToVersion, "upgrade"); err != nil { return fmt.Errorf("failed to commit upgrade branch: %w", err) } return nil } // mergeOriginalToUpgrade attempts to merge the upgrade branch func (opts *Update) mergeOriginalToUpgrade() (bool, error) { hasConflicts := false if err := helpers.GitCmd(opts.GitConfig, "checkout", "-b", opts.MergeBranch, opts.UpgradeBranch).Run(); err != nil { return hasConflicts, fmt.Errorf("failed to create merge branch %s from %s: %w", opts.MergeBranch, opts.UpgradeBranch, err) } checkoutCmd := helpers.GitCmd(opts.GitConfig, "checkout", opts.MergeBranch) if err := checkoutCmd.Run(); err != nil { return hasConflicts, fmt.Errorf("failed to checkout base branch %s: %w", opts.MergeBranch, err) } mergeCmd := helpers.GitCmd(opts.GitConfig, "merge", "--no-edit", "--no-commit", opts.OriginalBranch) err := mergeCmd.Run() if err != nil { var exitErr *exec.ExitError // If the merge has an error that is not a conflict, return an error 2 if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 { hasConflicts = true if !opts.Force { log.Warn("Merge stopped due to conflicts. Manual resolution is required.") log.Warn("After resolving the conflicts, run the following command:") log.Warn(" make manifests generate fmt vet lint-fix") log.Warn("This ensures manifests and generated files are up to date, and the project layout remains consistent.") return hasConflicts, fmt.Errorf("merge stopped due to conflicts") } log.Warn("Merge completed with conflicts. Conflict markers will be committed.") } else { return hasConflicts, fmt.Errorf("merge failed unexpectedly: %w", err) } } if !hasConflicts { log.Info("Merge happened without conflicts.") } // Best effort to run make targets to ensure the project is in a good state runMakeTargets(true) // Step 4: Stage and commit if err := helpers.GitCmd(opts.GitConfig, "add", "--all").Run(); err != nil { return hasConflicts, fmt.Errorf("failed to stage merge results: %w", err) } if err := helpers.CommitIgnoreEmpty(opts.getMergeMessage(hasConflicts), "merge"); err != nil { return hasConflicts, fmt.Errorf("failed to commit merge branch: %w", err) } log.Info("Merge completed") return hasConflicts, nil } func (opts *Update) getMergeMessage(hasConflicts bool) string { if hasConflicts { // Use custom conflict message if provided if opts.CommitMessageConflict != "" { return opts.CommitMessageConflict } // Otherwise use default conflict format return helpers.ConflictCommitMessage(opts.FromVersion, opts.ToVersion) } // Use custom commit message if provided if opts.CommitMessage != "" { return opts.CommitMessage } return helpers.MergeCommitMessage(opts.FromVersion, opts.ToVersion) } ================================================ FILE: internal/cli/alpha/internal/update/update_test.go ================================================ //go:build integration /* Copyright 2025 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 update import ( "fmt" "os" "path/filepath" "strings" "github.com/h2non/gock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/update/helpers" ) // Mock response for binary executables. func mockBinResponse(script, mockBin string) error { err := os.WriteFile(mockBin, []byte(script), 0o755) Expect(err).NotTo(HaveOccurred()) if err != nil { return fmt.Errorf("error Mocking bin response: %w", err) } return nil } // Mock response from an URL. func mockURLResponse(body, url string, times, reply int) { parts := strings.Split(url, "/") host := strings.Join(parts[0:3], "/") path := "/" + strings.Join(parts[3:], "/") gock.New(host).Get(path).Times(times).Reply(reply).Body(strings.NewReader(body)) } var _ = Describe("Prepare for internal update", func() { var ( tmpDir string mockGit string mockMake string mocksh string logFile string oldPath string err error opts Update ) BeforeEach(func() { opts = Update{ FromVersion: "v4.5.0", ToVersion: "v4.6.0", FromBranch: defaultBranch, } // Create temporary directory to house fake bin executables. tmpDir, err = os.MkdirTemp("", "temp-bin") Expect(err).NotTo(HaveOccurred()) // Common file to log command runs from the fake bin. logFile = filepath.Join(tmpDir, "bin.log") // Create fake bin executables. mockGit = filepath.Join(tmpDir, "git") mockMake = filepath.Join(tmpDir, "make") mocksh = filepath.Join(tmpDir, "sh") script := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 0` Expect(mockBinResponse(script, mockGit)).To(Succeed()) Expect(mockBinResponse(script, mockMake)).To(Succeed()) Expect(mockBinResponse(script, mocksh)).To(Succeed()) // Prepend temp bin directory to PATH env. oldPath = os.Getenv("PATH") Expect(os.Setenv("PATH", tmpDir+":"+oldPath)).To(Succeed()) // Mock GitHub release download. mockURLResponse(script, "https://github.com/kubernetes-sigs/kubebuilder/releases/download", 2, 200) }) AfterEach(func() { _ = os.RemoveAll(tmpDir) _ = os.Setenv("PATH", oldPath) defer gock.Off() }) // Helper that formats the expectations properly. verifyLogs := func(newBranch, oldBranch, fromVersion string) { logs, readErr := os.ReadFile(logFile) Expect(readErr).NotTo(HaveOccurred()) s := string(logs) Expect(s).To(ContainSubstring( fmt.Sprintf("checkout -b %s %s", newBranch, oldBranch), )) Expect(s).To(ContainSubstring(fmt.Sprintf("checkout %s", newBranch))) Expect(s).To(ContainSubstring( "-c find . -mindepth 1 -maxdepth 1 ! -name '.git' ! -name 'PROJECT' -exec rm -rf {}", )) Expect(s).To(ContainSubstring("alpha generate")) Expect(s).To(ContainSubstring("add --all")) Expect(s).To(ContainSubstring( fmt.Sprintf("initial scaffold from release version: %s", fromVersion), )) } Context("Update", func() { It("succeeds using a default three-way Git merge", func() { err = opts.Update() Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) Expect(string(logs)).To(ContainSubstring( fmt.Sprintf("checkout %s", opts.FromBranch), )) }) It("fails when git command fails", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mockGit)).To(Succeed()) err = opts.Update() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( fmt.Sprintf("failed to checkout base branch %s", opts.FromBranch), )) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) Expect(string(logs)).To(ContainSubstring( fmt.Sprintf("checkout %s", opts.FromBranch), )) }) It("fails when kubebuilder binary cannot be downloaded", func() { gock.Off() gock.New("https://github.com"). Get("/kubernetes-sigs/kubebuilder/releases/download"). Times(2).Reply(401).Body(strings.NewReader("")) err = opts.Update() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to prepare ancestor branch")) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) Expect(string(logs)).To(ContainSubstring( fmt.Sprintf("checkout %s", opts.FromBranch), )) }) }) Context("RegenerateProjectWithVersion", func() { It("succeeds downloading binary and running `alpha generate`", func() { err = regenerateProjectWithVersion(opts.FromVersion) Expect(err).ToNot(HaveOccurred()) }) It("fails downloading binary", func() { gock.Off() gock.New("https://github.com"). Get("/kubernetes-sigs/kubebuilder/releases/download"). Times(2).Reply(401).Body(strings.NewReader("")) err = regenerateProjectWithVersion(opts.FromVersion) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( fmt.Sprintf("failed to download release %s binary", opts.FromVersion), )) }) It("fails running alpha generate", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` gock.Off() gock.New("https://github.com"). Get("/kubernetes-sigs/kubebuilder/releases/download"). Times(2).Reply(200).Body(strings.NewReader(fail)) err = regenerateProjectWithVersion(opts.FromVersion) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( "failed to run alpha generate on ancestor branch", )) }) }) Context("PrepareAncestorBranch", func() { It("succeeds", func() { err = opts.prepareAncestorBranch() Expect(err).ToNot(HaveOccurred()) verifyLogs(opts.AncestorBranch, opts.FromBranch, opts.FromVersion) }) It("fails creating branch", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mockGit)).To(Succeed()) err = opts.prepareAncestorBranch() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( fmt.Sprintf("failed to create %s from %s", opts.AncestorBranch, opts.FromBranch), )) }) }) Context("PrepareUpgradeBranch", func() { It("succeeds", func() { err = opts.prepareUpgradeBranch() Expect(err).ToNot(HaveOccurred()) verifyLogs(opts.UpgradeBranch, opts.AncestorBranch, opts.ToVersion) }) It("fails creating branch", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mockGit)).To(Succeed()) err = opts.prepareUpgradeBranch() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( fmt.Sprintf("failed to checkout %s branch off %s", opts.UpgradeBranch, opts.AncestorBranch), )) }) }) Context("BinaryWithVersion", func() { It("succeeds to download the specified released version", func() { _, err = helpers.DownloadReleaseVersionWith(opts.FromVersion) Expect(err).ToNot(HaveOccurred()) }) It("fails to download the specified released version", func() { gock.Off() gock.New("https://github.com"). Get("/kubernetes-sigs/kubebuilder/releases/download"). Times(2).Reply(401).Body(strings.NewReader("")) _, err = helpers.DownloadReleaseVersionWith(opts.FromVersion) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal("failed to download the binary: HTTP 401")) }) }) Context("CleanupBranch", func() { It("succeeds executing cleanup command", func() { err = cleanupBranch() Expect(err).ToNot(HaveOccurred()) }) It("fails executing cleanup command", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mocksh)).To(Succeed()) err = cleanupBranch() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to clean up files")) }) }) Context("RunMakeTargets", func() { It("logs warning when make fails", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mockMake)).To(Succeed()) // Should not panic even if make fails; just logs a warning. runMakeTargets(false) }) }) Context("RunAlphaGenerate", func() { It("succeeds", func() { mockKB := filepath.Join(tmpDir, "kubebuilder") script := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 0` Expect(mockBinResponse(script, mockKB)).To(Succeed()) err = runAlphaGenerate(tmpDir, opts.FromVersion) Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).NotTo(HaveOccurred()) Expect(string(logs)).To(ContainSubstring("alpha generate")) }) It("fails", func() { mockKB := filepath.Join(tmpDir, "kubebuilder") fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mockKB)).To(Succeed()) err = runAlphaGenerate(tmpDir, opts.FromVersion) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to run alpha generate")) }) }) Context("PrepareOriginalBranch", func() { It("succeeds", func() { err = opts.prepareOriginalBranch() Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) s := string(logs) Expect(s).To(ContainSubstring( fmt.Sprintf("checkout -b %s", opts.OriginalBranch), )) Expect(s).To(ContainSubstring( fmt.Sprintf("checkout %s -- .", opts.FromBranch), )) Expect(s).To(ContainSubstring("add --all")) Expect(s).To(ContainSubstring( fmt.Sprintf("original code from %s to keep changes", opts.FromBranch), )) }) It("fails", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mockGit)).To(Succeed()) err = opts.prepareOriginalBranch() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( fmt.Sprintf("failed to checkout branch %s", opts.OriginalBranch), )) }) }) Context("MergeOriginalToUpgrade", func() { BeforeEach(func() { // deterministic names for merge test opts.UpgradeBranch = "tmp-upgrade-X" opts.MergeBranch = "tmp-merge-X" opts.OriginalBranch = "tmp-original-X" }) It("succeeds and commits with normal message", func() { _, err = opts.mergeOriginalToUpgrade() Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) s := string(logs) Expect(s).To(ContainSubstring( fmt.Sprintf("checkout -b %s %s", opts.MergeBranch, opts.UpgradeBranch), )) Expect(s).To(ContainSubstring(fmt.Sprintf("checkout %s", opts.MergeBranch))) Expect(s).To(ContainSubstring( fmt.Sprintf("merge --no-edit --no-commit %s", opts.OriginalBranch), )) Expect(s).To(ContainSubstring("add --all")) Expect(s).To(ContainSubstring(helpers.MergeCommitMessage(opts.FromVersion, opts.ToVersion))) }) It("fails when branch creation fails", func() { fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, mockGit)).To(Succeed()) _, err = opts.mergeOriginalToUpgrade() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring( fmt.Sprintf("failed to create merge branch %s from %s", opts.MergeBranch, opts.UpgradeBranch), )) }) It("stops on conflicts when Force=false", func() { failOnMerge := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "merge" ]]; then exit 1; fi exit 0` Expect(mockBinResponse(failOnMerge, mockGit)).To(Succeed()) opts.Force = false _, err = opts.mergeOriginalToUpgrade() Expect(err).To(HaveOccurred()) s, _ := os.ReadFile(logFile) Expect(string(s)).NotTo(ContainSubstring("commit --no-verify -m")) }) It("commits with conflict message when Force=true", func() { failOnMerge := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "merge" ]]; then exit 1; fi exit 0` Expect(mockBinResponse(failOnMerge, mockGit)).To(Succeed()) opts.Force = true _, err = opts.mergeOriginalToUpgrade() Expect(err).ToNot(HaveOccurred()) s, _ := os.ReadFile(logFile) Expect(string(s)).To(ContainSubstring( helpers.ConflictCommitMessage(opts.FromVersion, opts.ToVersion), )) }) }) Context("SquashToOutputBranch", func() { BeforeEach(func() { opts.FromBranch = defaultBranch opts.FromVersion = "v4.5.0" opts.ToVersion = "v4.6.0" if opts.MergeBranch == "" { opts.MergeBranch = "tmp-merge-test" } }) It("creates/resets output branch and commits one squashed snapshot", func() { opts.OutputBranch = "" // default naming opts.RestorePath = []string{".github/workflows"} opts.ShowCommits = false err = opts.squashToOutputBranch(false) // no conflicts Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) s := string(logs) Expect(s).To(ContainSubstring(fmt.Sprintf("checkout %s", opts.FromBranch))) expOut := fmt.Sprintf( "checkout -B %s %s", fmt.Sprintf("kubebuilder-update-from-%s-to-%s", opts.FromVersion, opts.ToVersion), opts.FromBranch, ) Expect(s).To(ContainSubstring(expOut)) Expect(s).To(ContainSubstring(fmt.Sprintf("checkout %s -- .", opts.MergeBranch))) Expect(s).To(ContainSubstring("add --all")) Expect(s).To(ContainSubstring(helpers.MergeCommitMessage(opts.FromVersion, opts.ToVersion))) Expect(s).To(ContainSubstring("commit --no-verify -m")) }) It("respects a custom output branch name", func() { opts.OutputBranch = "my-custom-branch" err = opts.squashToOutputBranch(false) Expect(err).ToNot(HaveOccurred()) logs, _ := os.ReadFile(logFile) Expect(string(logs)).To(ContainSubstring( fmt.Sprintf("checkout -B %s %s", "my-custom-branch", opts.FromBranch), )) }) It("no changes -> commit exits 1 but helper returns nil", func() { fake := `#!/bin/bash echo "$@" >> "` + logFile + `" if [[ "$1" == "commit" ]]; then exit 1; fi exit 0` Expect(mockBinResponse(fake, mockGit)).To(Succeed()) opts.RestorePath = nil Expect(opts.squashToOutputBranch(false)).To(Succeed()) s, _ := os.ReadFile(logFile) Expect(string(s)).To(ContainSubstring("commit --no-verify -m")) }) It("trims restore-path and skips blanks", func() { opts.RestorePath = []string{" .github/workflows ", "", "docs"} Expect(opts.squashToOutputBranch(false)).To(Succeed()) s, _ := os.ReadFile(logFile) Expect(string(s)).To(ContainSubstring("checkout main -- docs")) Expect(string(s)).To(ContainSubstring("checkout main -- .github/workflows")) }) }) Context("getOutputBranchName", func() { It("returns default name when OutputBranch is empty", func() { const fromVersion = "v4.5.0" const toVersion = "v4.6.0" opts.FromVersion = fromVersion opts.ToVersion = toVersion opts.OutputBranch = "" want := fmt.Sprintf("kubebuilder-update-from-%s-to-%s", fromVersion, toVersion) Expect(opts.getOutputBranchName()).To(Equal(want)) }) It("returns custom name when OutputBranch is set", func() { opts.OutputBranch = "my-custom" Expect(opts.getOutputBranchName()).To(Equal("my-custom")) }) }) Context("runAlphaGenerate PATH restoration", func() { It("does not mutate process PATH (same even on failure)", func() { tmp := filepath.Join(tmpDir, "kubebuilder") fail := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` Expect(mockBinResponse(fail, tmp)).To(Succeed()) orig := os.Getenv("PATH") err := runAlphaGenerate(tmpDir, "v4.5.0") Expect(err).To(HaveOccurred()) Expect(os.Getenv("PATH")).To(Equal(orig)) }) }) Context("getMergeMessage", func() { BeforeEach(func() { opts.FromVersion = "v4.5.0" opts.ToVersion = "v4.6.0" opts.CommitMessage = "" opts.CommitMessageConflict = "" }) It("uses custom commit message when provided (no conflicts)", func() { opts.CommitMessage = "chore: custom update message" msg := opts.getMergeMessage(false) Expect(msg).To(Equal("chore: custom update message")) }) It("uses custom conflict message when provided (with conflicts)", func() { opts.CommitMessageConflict = "chore: custom conflict message" msg := opts.getMergeMessage(true) Expect(msg).To(Equal("chore: custom conflict message")) }) It("uses default message when no custom message (no conflicts)", func() { msg := opts.getMergeMessage(false) Expect(msg).To(Equal(helpers.MergeCommitMessage(opts.FromVersion, opts.ToVersion))) }) It("uses default conflict message when no custom message (with conflicts)", func() { msg := opts.getMergeMessage(true) Expect(msg).To(Equal(helpers.ConflictCommitMessage(opts.FromVersion, opts.ToVersion))) }) It("prefers conflict message over regular message when conflicts occur", func() { opts.CommitMessage = "chore: regular message" opts.CommitMessageConflict = "chore: conflict message" msg := opts.getMergeMessage(true) Expect(msg).To(Equal("chore: conflict message")) }) It("falls back to default conflict message when only regular message is set", func() { opts.CommitMessage = "chore: regular message" msg := opts.getMergeMessage(true) Expect(msg).To(Equal(helpers.ConflictCommitMessage(opts.FromVersion, opts.ToVersion))) }) }) }) ================================================ FILE: internal/cli/alpha/internal/update/validate.go ================================================ /* Copyright 2025 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 update import ( "fmt" log "log/slog" "net/http" "os" "os/exec" "strings" "golang.org/x/mod/semver" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/update/helpers" ) // Validate checks the input info provided for the update and populates the cliVersion func (opts *Update) Validate() error { if err := opts.validateEqualVersions(); err != nil { return fmt.Errorf("failed to validate equal versions: %w", err) } if err := opts.validateGitRepo(); err != nil { return fmt.Errorf("failed to validate git repository: %w", err) } if err := opts.validateFromBranch(); err != nil { return fmt.Errorf("failed to validate --from-branch: %w", err) } if err := opts.validateSemanticVersions(); err != nil { return fmt.Errorf("failed to validate the versions: %w", err) } if err := validateReleaseAvailability(opts.FromVersion); err != nil { return fmt.Errorf("unable to find release %s: %w", opts.FromVersion, err) } if err := validateReleaseAvailability(opts.ToVersion); err != nil { return fmt.Errorf("unable to find release %s: %w", opts.ToVersion, err) } if opts.OpenGhIssue { if err := exec.Command("gh", "--version").Run(); err != nil { return fmt.Errorf("`gh` CLI not found or not authenticated. "+ "You must have gh instaled to use the --open-gh-issue option: %s", err) } } if opts.UseGhModels && !isGhModelsExtensionInstalled() { return fmt.Errorf("gh-models extension is not installed. To install the extension, run: " + "gh extension install https://github.com/github/gh-models") } return nil } // isGhModelsExtensionInstalled checks if the gh-models extension is installed func isGhModelsExtensionInstalled() bool { cmd := exec.Command("gh", "extension", "list") if _, err := cmd.Output(); err != nil { return false } return true } // validateGitRepo verifies if the current directory is a valid Git repository and checks for uncommitted changes. func (opts *Update) validateGitRepo() error { log.Info("Checking if is a git repository") gitCmd := exec.Command("git", "rev-parse", "--git-dir") if err := gitCmd.Run(); err != nil { return fmt.Errorf("not in a git repository") } log.Info("Checking if branch has uncommitted changes") gitCmd = exec.Command("git", "status", "--porcelain") output, err := gitCmd.Output() if err != nil { return fmt.Errorf("failed to check branch status: %w", err) } if len(strings.TrimSpace(string(output))) > 0 { return fmt.Errorf("working directory has uncommitted changes. " + "Please commit or stash them before updating") } return nil } // validateFromBranch the branch passed to the --from-branch flag func (opts *Update) validateFromBranch() error { // Check if the branch exists gitCmd := exec.Command("git", "rev-parse", "--verify", opts.FromBranch) if err := gitCmd.Run(); err != nil { return fmt.Errorf("%s branch does not exist locally. "+ "Run 'git branch -a' to see all available branches", opts.FromBranch) } return nil } // validateSemanticVersions the version informed by the user via --from-version flag func (opts *Update) validateSemanticVersions() error { if !semver.IsValid(opts.FromVersion) { return fmt.Errorf(" version informed (%s) has invalid semantic version. "+ "Expect: vX.Y.Z (Ex: v4.5.0)", opts.FromVersion) } if !semver.IsValid(opts.ToVersion) { return fmt.Errorf(" version informed (%s) has invalid semantic version. "+ "Expect: vX.Y.Z (Ex: v4.5.0)", opts.ToVersion) } return nil } // validateReleaseAvailability will verify if the binary to scaffold from-version flag is available func validateReleaseAvailability(version string) error { url := helpers.BuildReleaseURL(version) resp, err := http.Head(url) if err != nil { return fmt.Errorf("failed to check binary availability: %w", err) } defer func() { if err = resp.Body.Close(); err != nil { log.Error("failed to close connection", "error", err) } }() switch resp.StatusCode { case http.StatusOK: log.Info("Binary version available", "version", version) return nil case http.StatusNotFound: return fmt.Errorf("binary version %s not found. Check versions available in releases", version) default: return fmt.Errorf("unexpected response %d when checking binary availability for version %s", resp.StatusCode, version) } } // validateEqualVersions checks if from-version and to-version are the same. // If they are equal, logs an appropriate message and exits successfully. func (opts *Update) validateEqualVersions() error { if opts.FromVersion == opts.ToVersion { // Check if this is the latest version to provide appropriate message latestVersion, err := fetchLatestRelease() if err != nil { return fmt.Errorf("failed to fetch latest release for messaging: %w", err) } if opts.ToVersion == latestVersion { log.Info("Your project already uses the latest version. No action taken.", "version", opts.FromVersion) } else { log.Info("Your project already uses the specified version. No action taken.", "version", opts.FromVersion) } os.Exit(0) } return nil } ================================================ FILE: internal/cli/alpha/internal/update/validate_test.go ================================================ //go:build integration /* Copyright 2025 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 update import ( "os" "path/filepath" "strings" "github.com/h2non/gock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Prepare for internal update", func() { var ( tmpDir string mockGit string logFile string oldPath string err error opts *Update ) BeforeEach(func() { opts = &Update{ FromVersion: "v4.5.0", ToVersion: "v4.6.0", FromBranch: defaultBranch, OriginalBranch: "v4.6.0", } // Create temporary directory to house fake bin executables tmpDir, err = os.MkdirTemp("", "temp-bin") Expect(err).NotTo(HaveOccurred()) // Create a common file to log the command runs from the fake bin logFile = filepath.Join(tmpDir, "bin.log") // Create fake bin executables mockGit = filepath.Join(tmpDir, "git") script := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 0` err = mockBinResponse(script, mockGit) Expect(err).NotTo(HaveOccurred()) // Prepend temp bin directory to PATH env oldPath = os.Getenv("PATH") err = os.Setenv("PATH", tmpDir+":"+oldPath) Expect(err).NotTo(HaveOccurred()) gock.New("https://github.com"). Head("/kubernetes-sigs/kubebuilder/releases/download"). Times(2). Reply(200). Body(strings.NewReader("body")) }) AfterEach(func() { _ = os.RemoveAll(tmpDir) _ = os.Setenv("PATH", oldPath) defer gock.Off() }) Context("Validate", func() { It("Should scucceed", func() { err = opts.Validate() Expect(err).ToNot(HaveOccurred()) }) It("Should fail", func() { fakeBinScript := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` err = mockBinResponse(fakeBinScript, mockGit) Expect(err).ToNot(HaveOccurred()) err = opts.Validate() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("failed to validate git repository")) }) }) Context("ValidateGitRepo", func() { It("Should scucceed", func() { err = opts.validateGitRepo() Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) Expect(string(logs)).To(ContainSubstring("rev-parse --git-dir")) Expect(string(logs)).To(ContainSubstring("status --porcelain")) }) It("Should fail", func() { fakeBinScript := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` err = mockBinResponse(fakeBinScript, mockGit) Expect(err).ToNot(HaveOccurred()) err = opts.validateGitRepo() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("not in a git repository")) }) }) Context("ValidateFromBranch", func() { It("Should scucceed", func() { err = opts.validateFromBranch() Expect(err).ToNot(HaveOccurred()) logs, readErr := os.ReadFile(logFile) Expect(readErr).ToNot(HaveOccurred()) Expect(string(logs)).To(ContainSubstring("rev-parse --verify %s", opts.FromBranch)) }) It("Should fail", func() { fakeBinScript := `#!/bin/bash echo "$@" >> "` + logFile + `" exit 1` err = mockBinResponse(fakeBinScript, mockGit) Expect(err).ToNot(HaveOccurred()) err := opts.validateFromBranch() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("branch does not exist locally")) }) }) Context("ValidateSemanticVersions", func() { It("Should scucceed", func() { err := opts.validateSemanticVersions() Expect(err).ToNot(HaveOccurred()) }) It("Should fail", func() { opts.FromVersion = "6" err := opts.validateSemanticVersions() Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("has invalid semantic version. Expect: vX.Y.Z")) }) }) Context("ValidateReleaseAvailability", func() { It("Should scucceed", func() { err := validateReleaseAvailability(opts.ToVersion) Expect(err).ToNot(HaveOccurred()) }) It("Should fail", func() { gock.Off() gock.New("https://github.com"). Head("/kubernetes-sigs/kubebuilder/releases/download"). Reply(401). Body(strings.NewReader("body")) err := validateReleaseAvailability(opts.FromVersion) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("unexpected response")) }) }) }) ================================================ FILE: internal/cli/alpha/internal/update.go ================================================ /* Copyright 2025 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 internal import ( "fmt" "io" log "log/slog" "net/http" "os" "os/exec" "runtime" "strings" "github.com/spf13/afero" "golang.org/x/mod/semver" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" ) // Update contains configuration for the update operation type Update struct { // FromVersion specifies which version of Kubebuilder to use for the update. // If empty, the version from the PROJECT file will be used. FromVersion string // FromBranch specifies which branch to use as current when updating FromBranch string // CliVersion holds the version to be used during the upgrade process CliVersion string // BinaryURL holds the URL for downloading the specified binary from // the releases on GitHub BinaryURL string } // Update performs a complete project update by creating a three-way merge to help users // upgrade their Kubebuilder projects. The process creates multiple Git branches: // - ancestor: Clean state with old Kubebuilder version scaffolding // - current: User's current project state // - upgrade: New Kubebuilder version scaffolding // - merge: Attempts to merge upgrade changes into current state func (opts *Update) Update() error { // Download the specific Kubebuilder binary version for generating clean scaffolding tempDir, err := opts.downloadKubebuilderBinary() if err != nil { return fmt.Errorf("failed to download Kubebuilder %s binary: %w", opts.CliVersion, err) } log.Info("Downloaded binary kept for debugging purposes", "directory", tempDir) // Create ancestor branch with clean state for three-way merge if err := opts.checkoutAncestorBranch(); err != nil { return fmt.Errorf("failed to checkout the ancestor branch: %w", err) } // Remove all existing files to create a clean slate for re-scaffolding if err := opts.cleanUpAncestorBranch(); err != nil { return fmt.Errorf("failed to clean up the ancestor branch: %w", err) } // Generate clean scaffolding using the old Kubebuilder version if err := opts.runAlphaGenerate(tempDir, opts.CliVersion); err != nil { return fmt.Errorf("failed to run alpha generate on ancestor branch: %w", err) } // Create current branch representing user's existing project state if err := opts.checkoutCurrentOffAncestor(); err != nil { return fmt.Errorf("failed to checkout current off ancestor: %w", err) } // Create upgrade branch with new Kubebuilder version scaffolding if err := opts.checkoutUpgradeOffAncestor(); err != nil { return fmt.Errorf("failed to checkout upgrade off ancestor: %w", err) } // Create merge branch to attempt automatic merging of changes if err := opts.checkoutMergeOffCurrent(); err != nil { return fmt.Errorf("failed to checkout merge branch off current: %w", err) } // Attempt to merge upgrade changes into the user's current state if err := opts.mergeUpgradeIntoMerge(); err != nil { return fmt.Errorf("failed to merge upgrade into merge branch: %w", err) } return nil } // downloadKubebuilderBinary downloads the specified version of Kubebuilder binary // from GitHub releases and saves it to a temporary directory with executable permissions. // Returns the temporary directory path containing the binary. func (opts *Update) downloadKubebuilderBinary() (string, error) { // Construct GitHub release URL based on current OS and architecture url := opts.BinaryURL log.Info("Downloading the Kubebuilder binary", "version", opts.CliVersion, "download_url", url) // Create temporary directory for storing the downloaded binary fs := afero.NewOsFs() tempDir, err := afero.TempDir(fs, "", "kubebuilder"+opts.CliVersion+"-") if err != nil { return "", fmt.Errorf("failed to create temporary directory: %w", err) } // Create the binary file in the temporary directory binaryPath := tempDir + "/kubebuilder" file, err := os.Create(binaryPath) if err != nil { return "", fmt.Errorf("failed to create the binary file: %w", err) } defer func() { if err = file.Close(); err != nil { log.Error("failed to close the file", "error", err) } }() // Download the binary from GitHub releases response, err := http.Get(url) if err != nil { return "", fmt.Errorf("failed to download the binary: %w", err) } defer func() { if err = response.Body.Close(); err != nil { log.Error("failed to close the connection", "error", err) } }() // Check if download was successful if response.StatusCode != http.StatusOK { return "", fmt.Errorf("failed to download the binary: HTTP %d", response.StatusCode) } // Copy the downloaded content to the local file _, err = io.Copy(file, response.Body) if err != nil { return "", fmt.Errorf("failed to write the binary content to file: %w", err) } // Make the binary executable if err := os.Chmod(binaryPath, 0o755); err != nil { return "", fmt.Errorf("failed to make binary executable: %w", err) } log.Info("Kubebuilder successfully downloaded", "kubebuilder_version", opts.CliVersion, "binary_path", binaryPath) return tempDir, nil } // checkoutAncestorBranch creates and switches to the 'ancestor' branch. // This branch will serve as the common ancestor for the three-way merge, // containing clean scaffolding from the old Kubebuilder version. func (opts *Update) checkoutAncestorBranch() error { gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-ancestor") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to create and checkout ancestor branch: %w", err) } log.Info("Created and checked out ancestor branch") return nil } // cleanUpAncestorBranch removes all files from the ancestor branch to create // a clean state for re-scaffolding. This ensures the ancestor branch only // contains pure scaffolding without any user modifications. func (opts *Update) cleanUpAncestorBranch() error { log.Info("Cleaning all files and folders except .git and PROJECT") // Remove all tracked files from the Git repository cmd := exec.Command("find", ".", "-mindepth", "1", "-maxdepth", "1", "!", "-name", ".git", "!", "-name", "PROJECT", "-exec", "rm", "-rf", "{}", "+") log.Info("Running cleanup command", "command", cmd.Args) if err := cmd.Run(); err != nil { return fmt.Errorf("failed to clean up files in ancestor branch: %w", err) } log.Info("Successfully cleanup files in ancestor branch") // Remove all untracked files and directories gitCmd := exec.Command("git", "add", ".") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes in ancestor: %w", err) } log.Info("Successfully staged changes in ancestor") // Commit the cleanup to establish the clean state gitCmd = exec.Command("git", "commit", "-m", "Clean up the ancestor branch") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to commit the cleanup in ancestor branch: %w", err) } log.Info("Successfully committed cleanup on ancestor") return nil } // runMakeTargets is a helper function to run make with the targets necessary // to ensure all the necessary components are generated, formatted and linted. func runMakeTargets() error { targets := []string{"manifests", "generate", "fmt", "vet", "lint-fix"} for _, target := range targets { err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target) if err != nil { return fmt.Errorf("make %s failed: %v", target, err) } } return nil } // runAlphaGenerate executes the old Kubebuilder version's 'alpha generate' command // to create clean scaffolding in the ancestor branch. This uses the downloaded // binary with the original PROJECT file to recreate the project's initial state. func (opts *Update) runAlphaGenerate(tempDir, version string) error { // Temporarily modify PATH to use the downloaded Kubebuilder binary tempBinaryPath := tempDir + "/kubebuilder" originalPath := os.Getenv("PATH") tempEnvPath := tempDir + ":" + originalPath if err := os.Setenv("PATH", tempEnvPath); err != nil { return fmt.Errorf("failed to set temporary PATH: %w", err) } // Restore original PATH when function completes defer func() { if err := os.Setenv("PATH", originalPath); err != nil { log.Error("failed to restore original PATH", "error", err) } }() // Prepare the alpha generate command with proper I/O redirection cmd := exec.Command(tempBinaryPath, "alpha", "generate") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = os.Environ() // Execute the alpha generate command to create clean scaffolding if err := cmd.Run(); err != nil { return fmt.Errorf("failed to run alpha generate: %w", err) } log.Info("Successfully ran alpha generate using Kubebuilder", "version", version) // Run make targets to ensure all the necessary components are generated, // formatted and linted. log.Info("Running 'make manifests generate fmt vet lint-fix'") if err := runMakeTargets(); err != nil { return fmt.Errorf("failed to run make: %w", err) } log.Info("Successfully ran make targets in ancestor") // Stage all generated files gitCmd := exec.Command("git", "add", ".") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes in ancestor: %w", err) } log.Info("Successfully staged all changes in ancestor") // Commit the re-scaffolded project to the ancestor branch gitCmd = exec.Command("git", "commit", "-m", "Re-scaffold in ancestor") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to commit changes in ancestor: %w", err) } log.Info("Successfully committed changes in ancestor") return nil } // checkoutCurrentOffAncestor creates the 'current' branch from ancestor and // populates it with the user's actual project content from the default branch. // This represents the current state of the user's project. func (opts *Update) checkoutCurrentOffAncestor() error { // Create current branch starting from the clean ancestor state gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-current", "tmp-kb-update-ancestor") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to checkout current branch off ancestor: %w", err) } log.Info("Successfully checked out current branch off ancestor") // Overlay the user's actual project content from default branch gitCmd = exec.Command("git", "checkout", opts.FromBranch, "--", ".") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to checkout content from default branch onto current: %w", err) } log.Info("Successfully checked out content from main onto current branch") // Stage all the user's current project content gitCmd = exec.Command("git", "add", ".") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage all changes in current: %w", err) } log.Info("Successfully staged all changes in current") // Commit the user's current state to the current branch gitCmd = exec.Command("git", "commit", "-m", "Add content from main onto current branch") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to commit changes: %w", err) } log.Info("Successfully committed changes in current") return nil } // checkoutUpgradeOffAncestor creates the 'upgrade' branch from ancestor and // generates fresh scaffolding using the current (latest) Kubebuilder version. // This represents what the project should look like with the new version. func (opts *Update) checkoutUpgradeOffAncestor() error { // Create upgrade branch starting from the clean ancestor state gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-upgrade", "tmp-kb-update-ancestor") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to checkout upgrade branch off ancestor: %w", err) } log.Info("Successfully checked out upgrade branch off ancestor") // Run alpha generate with the current (new) Kubebuilder version // This uses the system's installed kubebuilder binary cmd := exec.Command("kubebuilder", "alpha", "generate") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("failed to run alpha generate on upgrade branch: %w", err) } log.Info("Successfully ran alpha generate on upgrade branch") // Run make targets to ensure all the necessary components are generated, // formatted and linted. log.Info("Running 'make manifests generate fmt vet lint-fix'") if err := runMakeTargets(); err != nil { return fmt.Errorf("failed to run make: %w", err) } log.Info("Successfully ran make targets in upgrade") // Stage all the newly generated files gitCmd = exec.Command("git", "add", ".") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to stage changes on upgrade: %w", err) } log.Info("Successfully staged all changes in upgrade branch") // Commit the new version's scaffolding to the upgrade branch gitCmd = exec.Command("git", "commit", "-m", "alpha generate in upgrade branch") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to commit changes in upgrade branch: %w", err) } log.Info("Successfully committed changes in upgrade branch") return nil } // checkoutMergeOffCurrent creates the 'merge' branch from the current branch. // This branch will be used to attempt automatic merging of upgrade changes // with the user's current project state. func (opts *Update) checkoutMergeOffCurrent() error { gitCmd := exec.Command("git", "checkout", "-b", "tmp-kb-update-merge", "tmp-kb-update-current") if err := gitCmd.Run(); err != nil { return fmt.Errorf("failed to checkout merge branch off current: %w", err) } return nil } // mergeUpgradeIntoMerge attempts to merge the upgrade branch (containing new // Kubebuilder scaffolding) into the merge branch (containing user's current state). // If conflicts occur, it warns the user to resolve them manually rather than failing. func (opts *Update) mergeUpgradeIntoMerge() error { gitCmd := exec.Command("git", "merge", "upgrade") err := gitCmd.Run() if err != nil { // Check if the error is due to merge conflicts (exit code 1) if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { log.Warn("Merge with conflicts. Please resolve them manually") return nil // Don't treat conflicts as fatal errors } return fmt.Errorf("failed to merge the upgrade branch into the merge branch: %w", err) } // Run make targets to ensure all the necessary components are generated, // formatted and linted. log.Info("Running 'make manifests generate fmt vet lint-fix'") if err := runMakeTargets(); err != nil { return fmt.Errorf("failed to run make: %w", err) } log.Info("Successfully ran make targets in merge") return nil } // Validate checks if the user is in a git repository and if the repository is in a clean state. // It also validates if the version specified by the user is in a valid format and available for // download as a binary. func (opts *Update) Validate() error { // Validate git repository if err := opts.validateGitRepo(); err != nil { return fmt.Errorf("failed to validate git repository: %w", err) } // Validate --from-branch if err := opts.validateFromBranch(); err != nil { return fmt.Errorf("failed to validate --from-branch: %w", err) } // Load the PROJECT configuration file projectConfigFile, err := opts.loadConfigFile() if err != nil { return fmt.Errorf("failed to load the PROJECT file: %w", err) } // Extract the cliVersion field from the PROJECT file opts.CliVersion = projectConfigFile.Config().GetCliVersion() // Determine which Kubebuilder version to use for the update if err := opts.defineFromVersion(); err != nil { return fmt.Errorf("failed to define version: %w", err) } // Validate if the specified version is available as a binary in the releases if err := opts.validateBinaryAvailability(); err != nil { return fmt.Errorf("failed to validate binary availability: %w", err) } return nil } // Load the PROJECT configuration file to get the current CLI version func (opts *Update) loadConfigFile() (store.Store, error) { projectConfigFile := yaml.New(machinery.Filesystem{FS: afero.NewOsFs()}) // TODO: assess if DefaultPath could be renamed to a more self-descriptive name if err := projectConfigFile.LoadFrom(yaml.DefaultPath); err != nil { if _, statErr := os.Stat(yaml.DefaultPath); os.IsNotExist(statErr) { return projectConfigFile, fmt.Errorf("no PROJECT file found. Make sure you're in the project root directory") } return projectConfigFile, fmt.Errorf("fail to load the PROJECT file: %w", err) } return projectConfigFile, nil } // Define the version of the binary to be downloaded func (opts *Update) defineFromVersion() error { // Allow override of the version from PROJECT file via command line flag if opts.FromVersion != "" { if !semver.IsValid(opts.FromVersion) { return fmt.Errorf("invalid semantic version. Expect: vX.Y.Z (Ex: v4.5.0)") } opts.CliVersion = opts.FromVersion } if opts.CliVersion == "" { return fmt.Errorf("failed to retrieve Kubebuilder version from PROJECT file. Please use --from-version to inform it") } return nil } // Validate if the version specified is available as a binary for download // from the releases func (opts *Update) validateBinaryAvailability() error { // Ensure version has 'v' prefix for consistency with GitHub releases if !strings.HasPrefix(opts.CliVersion, "v") { opts.CliVersion = "v" + opts.CliVersion } // Construct the URL for pulling the binary from GitHub releases opts.BinaryURL = fmt.Sprintf("https://github.com/kubernetes-sigs/kubebuilder/releases/download/%s/kubebuilder_%s_%s", opts.CliVersion, runtime.GOOS, runtime.GOARCH) resp, err := http.Head(opts.BinaryURL) if err != nil { return fmt.Errorf("failed to check binary availability: %w", err) } defer func() { if err = resp.Body.Close(); err != nil { log.Error("failed to close connection", "error", err) } }() switch resp.StatusCode { case http.StatusOK: log.Info("Binary version available", "version", opts.CliVersion) return nil case http.StatusNotFound: return fmt.Errorf("binary version %s not found. Check versions available in releases", opts.CliVersion) default: return fmt.Errorf("unexpected response %d when checking binary availability for version %s", resp.StatusCode, opts.CliVersion) } } // Validate if in a git repository with clean state func (opts *Update) validateGitRepo() error { // Check if in a git repository gitCmd := exec.Command("git", "rev-parse", "--git-dir") if err := gitCmd.Run(); err != nil { return fmt.Errorf("not in a git repository") } // Check if the branch has uncommitted changes gitCmd = exec.Command("git", "status", "--porcelain") output, err := gitCmd.Output() if err != nil { return fmt.Errorf("failed to check branch status: %w", err) } if len(strings.TrimSpace(string(output))) > 0 { return fmt.Errorf("working directory has uncommitted changes. Please commit or stash them before updating") } return nil } // Validate the branch passed to the --from-branch flag func (opts *Update) validateFromBranch() error { // Set default if not specified if opts.FromBranch == "" { opts.FromBranch = "main" } // Check if the branch exists gitCmd := exec.Command("git", "rev-parse", "--verify", opts.FromBranch) if err := gitCmd.Run(); err != nil { return fmt.Errorf("%s branch does not exist locally. Run 'git branch -a' to see all available branches", opts.FromBranch) } return nil } ================================================ FILE: internal/cli/alpha/suite_test.go ================================================ /* Copyright 2025 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. */ //lint:ignore ST1001 we use dot-imports in tests for brevity package alpha import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) // Figuring out ways to test run these tests similar to existing. // Currently unable to run without this on VSCode. Will remove once done func TestCommand(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "suite test for alpha commands") } ================================================ FILE: internal/cli/alpha/update.go ================================================ /* Copyright 2025 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 alpha import ( "fmt" "log/slog" "os" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha/internal/update" ) // NewUpdateCommand creates and returns a new Cobra command for updating Kubebuilder projects. func NewUpdateCommand() *cobra.Command { opts := update.Update{} var gitCfg []string updateCmd := &cobra.Command{ Use: "update", Short: "Update your project to a newer version (3-way merge; squash by default)", Long: `Upgrade your project scaffold using a 3-way merge while preserving your code. The updater uses four temporary branches during the run: • ancestor : clean scaffold from the starting version (--from-version) • original : snapshot of your current project (--from-branch) • upgrade : scaffold generated with the target version (--to-version) • merge : result of merging original into upgrade (conflicts possible) Output branch & history: • Default: SQUASH the merge result into ONE commit on: kubebuilder-update-from--to- • --show-commits: keep full history (not compatible with --restore-path). Conflicts: • Default: stop on conflicts and leave the merge branch for manual resolution. • --force: commit with conflict markers so automation can proceed. Other options: • --restore-path: restore paths from base when squashing (e.g., CI configs). • --output-branch: override the output branch name. • --merge-message: custom commit message for clean merges. • --conflict-message: custom commit message for merges with conflicts. • --push: push the output branch to 'origin' after the update. • --git-config: pass per-invocation Git config as -c key=value (repeatable). When not set, defaults are set to improve detection during merges. Defaults: • --from-version / --to-version: resolved from PROJECT and the latest release if unset. • --from-branch: defaults to 'main' if not specified.`, Example: ` # Update from the version in PROJECT to the latest, stop on conflicts kubebuilder alpha update # Update from a specific version to latest kubebuilder alpha update --from-version v4.6.0 # Update from v4.5.0 to v4.7.0 and keep conflict markers (automation-friendly) kubebuilder alpha update --from-version v4.5.0 --to-version v4.7.0 --force # Keep full commit history instead of squashing kubebuilder alpha update --from-version v4.5.0 --to-version v4.7.0 --force --show-commits # Squash while preserving CI workflows from base (e.g., main) kubebuilder alpha update --force --restore-path .github/workflows # Show commits into a custom output branch name kubebuilder alpha update --force --show-commits --output-branch my-update-branch # Run update and push the output branch to origin (works with or without --show-commits) kubebuilder alpha update --from-version v4.6.0 --to-version v4.7.0 --force --push # Use custom commit messages for both scenarios kubebuilder alpha update --force \ --merge-message "chore: upgrade kubebuilder scaffold" \ --conflict-message "chore: upgrade with conflicts - manual review needed" # Create an issue and add an AI overview comment kubebuilder alpha update --open-gh-issue --use-gh-models # Add extra Git configs (no need to re-specify defaults) kubebuilder alpha update --git-config merge.conflictStyle=diff3 --git-config rerere.enabled=true # Disable Git config defaults completely, use only custom configs kubebuilder alpha update --git-config disable --git-config rerere.enabled=true`, PreRunE: func(_ *cobra.Command, _ []string) error { if opts.ShowCommits && len(opts.RestorePath) > 0 { return fmt.Errorf("the --restore-path flag is not supported with --show-commits") } if opts.UseGhModels && !opts.OpenGhIssue { return fmt.Errorf("the --use-gh-models requires --open-gh-issue to be set") } // Defaults always on unless "disable" is present anywhere defaults := []string{ "merge.renameLimit=999999", "diff.renameLimit=999999", "merge.conflictStyle=merge", } hasDisable := false filtered := make([]string, 0, len(gitCfg)) for _, v := range gitCfg { if v == "disable" { hasDisable = true continue } filtered = append(filtered, v) } if hasDisable { // no defaults; only user-provided configs (excluding "disable") opts.GitConfig = filtered } else { // defaults + user configs (user can override by repeating keys) opts.GitConfig = append(defaults, filtered...) } if err := opts.Prepare(); err != nil { return fmt.Errorf("failed to prepare update: %w", err) } return opts.Validate() }, Run: func(_ *cobra.Command, _ []string) { if err := opts.Update(); err != nil { slog.Error("Update failed", "error", err) os.Exit(1) } }, } updateCmd.Flags().StringVar(&opts.FromVersion, "from-version", "", "binary release version to upgrade from. Should match the version used to init the project and be "+ "a valid release version, e.g., v4.6.0. If not set, it defaults to the version specified in the PROJECT file.") updateCmd.Flags().StringVar(&opts.ToVersion, "to-version", "", "binary release version to upgrade to. Should be a valid release version, e.g., v4.7.0. "+ "If not set, it defaults to the latest release version available in the project repository.") updateCmd.Flags().StringVar(&opts.FromBranch, "from-branch", "", "Git branch to use as current state of the project for the update.") updateCmd.Flags().BoolVar(&opts.Force, "force", false, "Force the update even if conflicts occur. Conflicted files will include conflict markers, and a "+ "commit will be created automatically. Ideal for automation (e.g., cronjobs, CI).") updateCmd.Flags().BoolVar(&opts.ShowCommits, "show-commits", false, "If set, the update will keep the full history instead of squashing into a single commit.") updateCmd.Flags().StringArrayVar(&opts.RestorePath, "restore-path", nil, "Paths to preserve from the base branch (repeatable). Not supported with --show-commits.") updateCmd.Flags().StringVar(&opts.OutputBranch, "output-branch", "", "Override the default output branch name (default: kubebuilder-update-from--to-).") updateCmd.Flags().BoolVar(&opts.Push, "push", false, "Push the output branch to the remote repository after the update.") updateCmd.Flags().StringVar(&opts.CommitMessage, "merge-message", "", "Custom commit message for successful merges (no conflicts). "+ "Defaults to 'chore(kubebuilder): update scaffold -> '.") updateCmd.Flags().StringVar(&opts.CommitMessageConflict, "conflict-message", "", "Custom commit message for merges with conflicts. "+ "Defaults to 'chore(kubebuilder): (:warning: manual conflict resolution required) update scaffold -> '.") updateCmd.Flags().BoolVar(&opts.OpenGhIssue, "open-gh-issue", false, "Create a GitHub issue with a pre-filled checklist and compare link after the update completes (requires `gh`).") updateCmd.Flags().BoolVar( &opts.UseGhModels, "use-gh-models", false, "Generate and post an AI summary comment to the GitHub Issue using `gh models run`. "+ "Requires --open-gh-issue and GitHub CLI (`gh`) with the `gh-models` extension.") updateCmd.Flags().StringArrayVar( &gitCfg, "git-config", nil, "Per-invocation Git config (repeatable). "+ "Defaults: -c merge.renameLimit=999999 -c diff.renameLimit=999999 -c merge.conflictStyle=merge. "+ "Your configs are applied on top. To disable defaults, include `--git-config disable`") return updateCmd } ================================================ FILE: internal/cli/alpha/update_test.go ================================================ /* Copyright 2025 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 alpha import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("NewUpdateCommand", func() { When("NewUpdateCommand", func() { It("Testing the NewUpdateCommand", func() { cmd := NewUpdateCommand() Expect(cmd).NotTo(BeNil()) Expect(cmd.Use).To(ContainSubstring("update")) Expect(cmd.Short).NotTo(Equal("")) Expect(cmd.Short).To(ContainSubstring("Update your project to a newer version")) flags := cmd.Flags() Expect(flags.Lookup("from-version")).NotTo(BeNil()) Expect(flags.Lookup("to-version")).NotTo(BeNil()) Expect(flags.Lookup("from-branch")).NotTo(BeNil()) Expect(flags.Lookup("force")).NotTo(BeNil()) Expect(flags.Lookup("show-commits")).NotTo(BeNil()) Expect(flags.Lookup("restore-path")).NotTo(BeNil()) Expect(flags.Lookup("output-branch")).NotTo(BeNil()) Expect(flags.Lookup("push")).NotTo(BeNil()) }) }) }) ================================================ FILE: internal/cli/cmd/cmd.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 cmd import ( "log/slog" "os" "github.com/spf13/afero" "sigs.k8s.io/kubebuilder/v4/internal/cli/version" "sigs.k8s.io/kubebuilder/v4/internal/logging" "sigs.k8s.io/kubebuilder/v4/pkg/cli" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang" deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1" golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4" autoupdatev1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha" grafanav1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha" helmv1alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha" helmv2alpha "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha" ) // Run bootstraps & runs the CLI func Run() { // Initialize custom logging handler FIRST - applies to ALL CLI operations opts := logging.HandlerOptions{ SlogOpts: slog.HandlerOptions{ Level: slog.LevelInfo, }, } handler := logging.NewHandler(os.Stdout, opts) logger := slog.New(handler) slog.SetDefault(logger) // Bundle plugin which built the golang projects scaffold with base.go/v4 and kustomize/v2 plugins gov4Bundle, _ := plugin.NewBundleWithOptions(plugin.WithName(golang.DefaultNameQualifier), plugin.WithVersion(plugin.Version{Number: 4}), plugin.WithPlugins(kustomizecommonv2.Plugin{}, golangv4.Plugin{}), plugin.WithDescription("Default scaffold (go/v4 + kustomize/v2)"), ) fs := machinery.Filesystem{ FS: afero.NewOsFs(), } externalPlugins, err := cli.DiscoverExternalPlugins(fs.FS) if err != nil { slog.Error("error discovering external plugins", "error", err) } v := version.New() c, err := cli.New( cli.WithCommandName("kubebuilder"), cli.WithVersion(v.PrintVersion()), cli.WithCliVersion(v.GetKubeBuilderVersion()), cli.WithPlugins( golangv4.Plugin{}, gov4Bundle, &kustomizecommonv2.Plugin{}, &deployimagev1alpha1.Plugin{}, &grafanav1alpha.Plugin{}, &helmv1alpha.Plugin{}, &helmv2alpha.Plugin{}, &autoupdatev1alpha.Plugin{}, ), cli.WithPlugins(externalPlugins...), cli.WithDefaultPlugins(cfgv3.Version, gov4Bundle), cli.WithDefaultProjectVersion(cfgv3.Version), cli.WithCompletion(), ) if err != nil { slog.Error("failed to create CLI", "error", err) os.Exit(1) } if err := c.Run(); err != nil { slog.Error("CLI run failed", "error", err) os.Exit(1) } } ================================================ FILE: internal/cli/version/version.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 version import ( "fmt" "os" "runtime" "runtime/debug" "strings" ) const ( unknown = "unknown" develVersion = "(devel)" kubernetesVendorVersion = "1.35.0" ) type Version struct { KubeBuilderVersion string `json:"kubeBuilderVersion"` KubernetesVendor string `json:"kubernetesVendor"` GitCommit string `json:"gitCommit"` BuildDate string `json:"buildDate"` GoOs string `json:"goOs"` GoArch string `json:"goArch"` } func New() Version { v := Version{ KubeBuilderVersion: develVersion, KubernetesVendor: kubernetesVendorVersion, GitCommit: unknown, BuildDate: unknown, GoOs: runtime.GOOS, GoArch: runtime.GOARCH, } if info, ok := debug.ReadBuildInfo(); ok { v.KubeBuilderVersion = resolveMainVersion(info.Main) v.applyVCSMetadata(info.Settings) } if testVersion := os.Getenv("KUBEBUILDER_TEST_VERSION"); testVersion != "" { v.KubeBuilderVersion = testVersion v.GitCommit = "test-commit" v.BuildDate = "1970-01-01T00:00:00Z" } return v } // GetKubeBuilderVersion returns only the CLI version string. // Used for the cliVersion field in scaffolded PROJECT files. func (v Version) GetKubeBuilderVersion() string { return strings.TrimPrefix(v.KubeBuilderVersion, "v") } func resolveMainVersion(main debug.Module) string { if main.Version != "" { return main.Version } return develVersion } // isPseudoVersion reports whether a version is a pseudo-version // (e.g., v0.0.0-20191109021931-daa7c04131f5 or v1.2.4-0.20191109021931-daa7c04131f5) func isPseudoVersion(v string) bool { return strings.Contains(v, "-0.") } func (v *Version) applyVCSMetadata(settings []debug.BuildSetting) { var isDirty bool for _, s := range settings { switch s.Key { case "vcs.revision": v.GitCommit = s.Value case "vcs.time": v.BuildDate = s.Value case "vcs.modified": isDirty = (s.Value == "true") } } if isDirty { // For development builds (not proper releases), use develVersion to avoid // polluting PROJECT files with unstable -dirty version strings. // For tagged releases, ignore the dirty flag to support GoReleaser builds // that may create artifacts during the build process. if v.KubeBuilderVersion == develVersion || isPseudoVersion(v.KubeBuilderVersion) { v.KubeBuilderVersion = develVersion } // Note: We don't append -dirty to tagged release versions to support // GoReleaser and similar build tools that may modify files during build. if !strings.Contains(v.GitCommit, "dirty") { v.GitCommit += "-dirty" } } } func (v Version) PrintVersion() string { return fmt.Sprintf(`KubeBuilder: %s Kubernetes: %s Git Commit: %s Build Date: %s Go OS/Arch: %s/%s`, v.KubeBuilderVersion, v.KubernetesVendor, v.GitCommit, v.BuildDate, v.GoOs, v.GoArch, ) } ================================================ FILE: internal/cli/version/version_test.go ================================================ /* Copyright 2026 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 version import ( "runtime/debug" "strings" "testing" ) func TestNew(t *testing.T) { t.Run("Environment variable override", func(t *testing.T) { expectedVersion := "v9.9.9-test" t.Setenv("KUBEBUILDER_TEST_VERSION", expectedVersion) v := New() if v.KubeBuilderVersion != expectedVersion { t.Errorf("expected version %s, got %s", expectedVersion, v.KubeBuilderVersion) } if v.GitCommit != "test-commit" { t.Errorf("expected gitCommit 'test-commit', got %s", v.GitCommit) } }) t.Run("Fallback behavior", func(t *testing.T) { v := New() if v.KubernetesVendor != kubernetesVendorVersion { t.Errorf("expected vendor %s, got %s", kubernetesVendorVersion, v.KubernetesVendor) } if v.GoOs == "" || v.GoArch == "" { t.Error("GoOs or GoArch was not populated from runtime") } }) t.Run("VCS metadata resolution with tagged release", func(t *testing.T) { v := &Version{KubeBuilderVersion: "v1.0.0"} settings := []debug.BuildSetting{ {Key: "vcs.revision", Value: "abcdef123"}, {Key: "vcs.modified", Value: "true"}, } v.applyVCSMetadata(settings) if !strings.HasSuffix(v.GitCommit, "-dirty") { t.Errorf("expected commit to be dirty, got %s", v.GitCommit) } // For tagged releases, we ignore dirty flag to support GoReleaser builds if v.KubeBuilderVersion != "v1.0.0" { t.Errorf("expected version to remain v1.0.0, got %s", v.KubeBuilderVersion) } }) t.Run("Version string formatting", func(t *testing.T) { v := Version{KubeBuilderVersion: "v1.2.3"} cleanVersion := v.GetKubeBuilderVersion() if cleanVersion != "1.2.3" { t.Errorf("expected 1.2.3, got %s", cleanVersion) } }) } func TestGetKubeBuilderVersion(t *testing.T) { testCases := []struct { name string input string expected string }{ {"strips v prefix", "v1.35.0", "1.35.0"}, {"handles no prefix", "1.35.0", "1.35.0"}, {"preserves devel", "(devel)", "(devel)"}, {"handles empty", "", ""}, {"handles dirty suffix", "v1.35.0-dirty", "1.35.0-dirty"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { v := Version{KubeBuilderVersion: tc.input} if actual := v.GetKubeBuilderVersion(); actual != tc.expected { t.Errorf("GetKubeBuilderVersion(%s) = %s; want %s", tc.input, actual, tc.expected) } }) } } func TestResolveMainVersion(t *testing.T) { tests := []struct { name string input string expected string }{ {"Valid Tag", "v4.10.4", "v4.10.4"}, {"Development Build", "", develVersion}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { main := debug.Module{Version: tt.input} if got := resolveMainVersion(main); got != tt.expected { t.Errorf("resolveMainVersion() = %v, want %v", got, tt.expected) } }) } } func TestApplyVCSMetadata(t *testing.T) { tests := []struct { name string initialVersion string settings []debug.BuildSetting expectCommit string expectVersion string expectDate string }{ { name: "Clean release build", initialVersion: "v4.10.4", settings: []debug.BuildSetting{ {Key: "vcs.revision", Value: "abcdef123"}, {Key: "vcs.time", Value: "2025-12-27T18:00:00Z"}, {Key: "vcs.modified", Value: "false"}, }, expectCommit: "abcdef123", expectVersion: "v4.10.4", expectDate: "2025-12-27T18:00:00Z", }, { name: "Dirty development build", initialVersion: develVersion, settings: []debug.BuildSetting{ {Key: "vcs.revision", Value: "abcdef123"}, {Key: "vcs.modified", Value: "true"}, {Key: "vcs.time", Value: "2025-12-29T19:30:00Z"}, }, expectCommit: "abcdef123-dirty", expectVersion: "(devel)", expectDate: "2025-12-29T19:30:00Z", }, { name: "Dirty tagged release (GoReleaser scenario)", initialVersion: "v4.5.3-rc.1", settings: []debug.BuildSetting{ {Key: "vcs.revision", Value: "abcdef123"}, {Key: "vcs.modified", Value: "true"}, {Key: "vcs.time", Value: "2025-12-30T10:00:00Z"}, }, expectCommit: "abcdef123-dirty", expectVersion: "v4.5.3-rc.1", // Stays clean for tagged releases expectDate: "2025-12-30T10:00:00Z", }, { name: "Dirty pseudo-version", initialVersion: "v1.2.4-0.20191109021931-daa7c04131f5", settings: []debug.BuildSetting{ {Key: "vcs.revision", Value: "abcdef123"}, {Key: "vcs.modified", Value: "true"}, }, expectCommit: "abcdef123-dirty", expectVersion: "(devel)", // Pseudo-versions become (devel) when dirty expectDate: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { v := &Version{ KubeBuilderVersion: tt.initialVersion, } v.applyVCSMetadata(tt.settings) if v.GitCommit != tt.expectCommit { t.Errorf("GitCommit = %v, want %v", v.GitCommit, tt.expectCommit) } if v.KubeBuilderVersion != tt.expectVersion { t.Errorf("KubeBuilderVersion = %v, want %v", v.KubeBuilderVersion, tt.expectVersion) } if v.BuildDate != tt.expectDate { t.Errorf("BuildDate = %v, want %v", v.BuildDate, tt.expectDate) } }) } } func TestPrintVersion(t *testing.T) { v := Version{ KubeBuilderVersion: "v9.99.9", KubernetesVendor: "9.99.9", GitCommit: "9990f08847dd1", BuildDate: "1970-01-12T12:12:12Z", GoOs: "linux", GoArch: "amd64", } expectedOutput := `KubeBuilder: v9.99.9 Kubernetes: 9.99.9 Git Commit: 9990f08847dd1 Build Date: 1970-01-12T12:12:12Z Go OS/Arch: linux/amd64` actualOutput := v.PrintVersion() if actualOutput != expectedOutput { t.Errorf("different output in version subcommand.\nexpected:\n%v\ngot:\n%v", expectedOutput, actualOutput) } } ================================================ FILE: internal/logging/handler.go ================================================ /* Copyright 2025 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 logging import ( "context" "fmt" "io" "log" "log/slog" ) const ( ColorReset = "\033[0m" ColorError = "\033[31m" ColorWarn = "\033[33m" ColorInfo = "\033[36m" ColorDebug = "\033[32m" ) type HandlerOptions struct { SlogOpts slog.HandlerOptions } type Handler struct { slog.Handler l *log.Logger } func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool { return h.Handler.Enabled(ctx, level) } func (h *Handler) Handle(_ context.Context, r slog.Record) error { var color string switch r.Level { case slog.LevelDebug: color = ColorDebug + r.Level.String() + ColorReset case slog.LevelInfo: color = ColorInfo + r.Level.String() + ColorReset case slog.LevelWarn: color = ColorWarn + r.Level.String() + ColorReset case slog.LevelError: color = ColorError + r.Level.String() + ColorReset } attrs := "" r.Attrs(func(attr slog.Attr) bool { attrs += fmt.Sprintf("%s=%v", attr.Key, attr.Value.Any()) return true }) h.l.Println(color, r.Message, attrs) return nil } func (h *Handler) WithAttrs(_ []slog.Attr) slog.Handler { return h } func (h *Handler) WithGroup(_ string) slog.Handler { return h } func NewHandler(out io.Writer, opts HandlerOptions) *Handler { h := &Handler{ Handler: slog.NewTextHandler(out, &opts.SlogOpts), l: log.New(out, "", 0), } return h } ================================================ FILE: main.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import "sigs.k8s.io/kubebuilder/v4/internal/cli/cmd" func main() { cmd.Run() } ================================================ FILE: netlify.toml ================================================ [build] base = "docs/book" command = "GO_VERSION=1.25.0 ./install-and-build.sh" publish = "docs/book/book" functions = "docs/book/functions" # Disables Pretty URLs feature so it doesn't break mdBook's ToC logic [build.processing.html] pretty_urls = false # TODO(directxman12): I don't know why, but this (functions) stanza is in the # docs and local `netlify dev`, but the above one (under build) is used by the # online version :-/ # used to handle the split between v2 and v3+ download links [functions] # relative to base directory directory = "functions" # Standard Netlify redirects [[redirects]] from = "https://kubebuilder.netlify.com/*" to = "https://book.kubebuilder.io/:splat" status = 301 force = true # HTTP-to-HTTPS rules [[redirects]] from = "http://go.kubebuilder.io/*" to = "https://go.kubebuilder.io/:splat" status = 301 force = true [[redirects]] from = "http://kubebuilder.netlify.com/*" to = "http://book.kubebuilder.io/:splat" status = 301 force = true # kubebuilder binary (v3+) and tarball (< v3) redirects. [[redirects]] from = "https://go.kubebuilder.io/dl/*" to = "https://go.kubebuilder.io/releases/:splat" status = 301 force = true [[redirects]] from = "https://go.kubebuilder.io/releases" to = "https://github.com/kubernetes-sigs/kubebuilder/releases" status = 302 force = true # Development branch redirect. [[redirects]] from = "https://go.kubebuilder.io/releases/master/:os/:arch" to = "https://storage.googleapis.com/kubebuilder-release/kubebuilder_master_:os_:arch.tar.gz" status = 302 force = true # Latest redirects. [[redirects]] from = "https://go.kubebuilder.io/releases/latest" to = "https://github.com/kubernetes-sigs/kubebuilder/releases/latest" status = 302 force = true [[redirects]] from = "https://go.kubebuilder.io/releases/latest/:os" to = "https://go.kubebuilder.io/releases/latest/:os/amd64" status = 302 force = true [[redirects]] from = "https://go.kubebuilder.io/releases/latest/:os/:arch" to = "https://github.com/kubernetes-sigs/kubebuilder/releases/latest/download/kubebuilder_:os_:arch" status = 302 force = true # general release redirects [[redirects]] from = "https://go.kubebuilder.io/releases/:version" to = "https://github.com/kubernetes-sigs/kubebuilder/releases/v:version" status = 302 force = true [[redirects]] from = "https://go.kubebuilder.io/releases/:version/:os" to = "https://go.kubebuilder.io/releases/:version/:os/amd64" status = 302 force = true # release download redirect [[redirects]] from = "https://go.kubebuilder.io/releases/:version/:os/:arch" # I don't quite know why, but netlify (or at least the dev mode) *insists* # on eating every other query parameter, so just use paths instead to = "/.netlify/functions/handle-version/releases/:version/:os/:arch" # 200 --> don't redirect to the function then to whereever it says, # just pretend like the function is mounted directly here status = 200 force = true # Tools redirects. [[redirects]] from = "https://go.kubebuilder.io/test-tools" to = "https://storage.googleapis.com/kubebuilder-tools" status = 302 force = true [[redirects]] from = "https://go.kubebuilder.io/test-tools/:k8sversion" to = "https://storage.googleapis.com/kubebuilder-tools/?prefix=kubebuilder-tools-:k8sversion" status = 302 force = true [[redirects]] from = "https://go.kubebuilder.io/test-tools/:k8sversion/:os" to = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-:k8sversion-:os-amd64.tar.gz" status = 302 force = true [[redirects]] from = "https://go.kubebuilder.io/test-tools/:k8sversion/:os/:arch" to = "https://storage.googleapis.com/kubebuilder-tools/kubebuilder-tools-:k8sversion-:os-:arch.tar.gz" status = 302 force = true # custom 404 handling -- this may need to be last -- netlify docs are unclear [[redirects]] from = "/*" to = "/404.html" status = 404 ================================================ FILE: pkg/cli/alpha.go ================================================ /* Copyright 2022 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 cli import ( "fmt" "strings" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/internal/cli/alpha" ) const ( alphaCommand = "alpha" ) var alphaCommands = []*cobra.Command{ newAlphaCommand(), alpha.NewScaffoldCommand(), alpha.NewUpdateCommand(), } func newAlphaCommand() *cobra.Command { cmd := &cobra.Command{ // TODO: If we need to create alpha commands please add a new file for each command } return cmd } func (c *CLI) newAlphaCmd() *cobra.Command { cmd := &cobra.Command{ Use: alphaCommand, SuggestFor: []string{"experimental"}, Short: "Alpha-stage subcommands", Long: strings.TrimSpace(` Alpha subcommands are for unstable features. - Alpha subcommands are exploratory and may be removed without warning. - No backwards compatibility is provided for any alpha subcommands. `), } // TODO: Add alpha commands here if we need to have them for i := range alphaCommands { cmd.AddCommand(alphaCommands[i]) } return cmd } func (c *CLI) addAlphaCmd() { if (len(alphaCommands) + len(c.extraAlphaCommands)) > 0 { c.cmd.AddCommand(c.newAlphaCmd()) } } func (c *CLI) addExtraAlphaCommands() error { // Search for the alpha subcommand var cmds *cobra.Command for _, subCmd := range c.cmd.Commands() { if subCmd.Name() == alphaCommand { cmds = subCmd break } } if cmds == nil { return fmt.Errorf("no %q command found", alphaCommand) } for _, cmd := range c.extraAlphaCommands { for _, subCmd := range cmds.Commands() { if cmd.Name() == subCmd.Name() { return fmt.Errorf("command %q already exists", fmt.Sprintf("%s %s", alphaCommand, cmd.Name())) } } cmds.AddCommand(cmd) } return nil } ================================================ FILE: pkg/cli/api.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ //nolint:dupl package cli import ( "fmt" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) const apiErrorMsg = "failed to create API" func (c CLI) newCreateAPICmd() *cobra.Command { cmd := &cobra.Command{ Use: "api", Short: "Scaffold a Kubernetes API", Long: `Scaffold a Kubernetes API.`, RunE: errCmdFunc( fmt.Errorf("api subcommand requires an existing project"), ), } // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { cmdErr(cmd, noResolvedPluginError{}) return cmd } // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateAPI. subcommands := c.filterSubcommands( func(p plugin.Plugin) bool { _, isValid := p.(plugin.CreateAPI) return isValid }, func(p plugin.Plugin) plugin.Subcommand { return p.(plugin.CreateAPI).GetCreateAPISubcommand() }, ) // Verify that there is at least one remaining plugin. if len(subcommands) == 0 { cmdErr(cmd, noAvailablePluginError{"API creation"}) return cmd } c.applySubcommandHooks(cmd, subcommands, apiErrorMsg, false) // Append plugin table after metadata updates c.appendPluginTable(cmd, func(p plugin.Plugin) bool { _, isValid := p.(plugin.CreateAPI) return isValid }, "Available plugins that support 'create api'") return cmd } ================================================ FILE: pkg/cli/cli.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "errors" "fmt" log "log/slog" "os" "strings" "github.com/spf13/afero" "github.com/spf13/cobra" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" yamlstore "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) const ( noticeColor = "\033[1;33m%s\033[0m" deprecationFmt = "[Deprecation Notice] %s\n\n" pluginsFlag = "plugins" projectVersionFlag = "project-version" ) // CLI is the command line utility that is used to scaffold kubebuilder project files. type CLI struct { /* Fields set by Option */ // Root command name. It is injected downstream to provide correct help, usage, examples and errors. commandName string // Full CLI version string. version string // CLI version string (just the CLI version number, no extra information). cliVersion string // CLI root's command description. description string // Plugins registered in the CLI. plugins map[string]plugin.Plugin // Default plugins in case none is provided and a config file can't be found. defaultPlugins map[config.Version][]string // Default project version in case none is provided and a config file can't be found. defaultProjectVersion config.Version // Commands injected by options. extraCommands []*cobra.Command // Alpha commands injected by options. extraAlphaCommands []*cobra.Command // Whether to add a completion command to the CLI. completionCommand bool /* Internal fields */ // Plugin keys to scaffold with. pluginKeys []string // Project version to scaffold. projectVersion config.Version // A filtered set of plugins that should be used by command constructors. resolvedPlugins []plugin.Plugin // Root command. cmd *cobra.Command // Underlying fs fs machinery.Filesystem } // New creates a new CLI instance. // // It follows the functional options pattern in order to customize the resulting CLI. // // It returns an error if any of the provided options fails. As some processing needs // to be done, execution errors may be found here. Instead of returning an error, this // function will return a valid CLI that errors in Run so that help is provided to the // user. func New(options ...Option) (*CLI, error) { // Create the CLI. c, err := newCLI(options...) if err != nil { return nil, err } // Build the cmd tree. if err := c.buildCmd(); err != nil { c.cmd.RunE = errCmdFunc(err) return c, nil } // Add extra commands injected by options. if err := c.addExtraCommands(); err != nil { return nil, err } // Add extra alpha commands injected by options. if err := c.addExtraAlphaCommands(); err != nil { return nil, err } // Write deprecation notices after all commands have been constructed. c.printDeprecationWarnings() return c, nil } // newCLI creates a default CLI instance and applies the provided options. // It is as a separate function for test purposes. func newCLI(options ...Option) (*CLI, error) { // Default CLI options. c := &CLI{ commandName: "kubebuilder", description: `CLI tool for building Kubernetes extensions and tools. `, plugins: make(map[string]plugin.Plugin), defaultPlugins: make(map[config.Version][]string), fs: machinery.Filesystem{FS: afero.NewOsFs()}, } // Apply provided options. for _, option := range options { if err := option(c); err != nil { return nil, err } } return c, nil } // buildCmd creates the underlying cobra command and stores it internally. func (c *CLI) buildCmd() error { c.cmd = c.newRootCmd() var uve config.UnsupportedVersionError // Get project version and plugin keys. switch err := c.getInfo(); { case err == nil: case errors.As(err, &uve) && uve.Version.Compare(config.Version{Number: 3, Stage: stage.Alpha}) == 0: // Check if the corresponding stable version exists, set c.projectVersion and break stableVersion := config.Version{ Number: uve.Version.Number, } if config.IsRegistered(stableVersion) { // Use the stableVersion c.projectVersion = stableVersion } else { // stable version not registered, let's bail out return err } default: return err } // Resolve plugins for project version and plugin keys. if err := c.resolvePlugins(); err != nil { return err } // Add the subcommands c.addSubcommands() return nil } // getInfo obtains the plugin keys and project version resolving conflicts between the project config file and flags. func (c *CLI) getInfo() error { // Get plugin keys and project version from project configuration file // We discard the error if file doesn't exist because not being able to read a project configuration // file is not fatal for some commands. The ones that require it need to check its existence later. hasConfigFile := true if err := c.getInfoFromConfigFile(); errors.Is(err, os.ErrNotExist) { hasConfigFile = false } else if err != nil { return err } // We can't early return here in case a project configuration file was found because // this command call may override the project plugins. // Get project version and plugin info from flags if err := c.getInfoFromFlags(hasConfigFile); err != nil { return err } // Get project version and plugin info from defaults c.getInfoFromDefaults() return nil } // getInfoFromConfigFile obtains the project version and plugin keys from the project config file. func (c *CLI) getInfoFromConfigFile() error { // Read the project configuration file cfg := yamlstore.New(c.fs) // Workaround for https://github.com/kubernetes-sigs/kubebuilder/issues/4433 // // This allows the `kubebuilder alpha generate` command to work with old projects // that use plugin versions no longer supported (like go.kubebuilder.io/v3). // // We read the PROJECT file into memory and update the plugin version (e.g. from v3 to v4) // before the CLI tries to load it. This avoids errors during config loading // and lets users migrate their project layout from go/v3 to go/v4. if isAlphaGenerateCommand(os.Args[1:]) { // Patch raw file bytes before unmarshalling if err := patchProjectFileInMemoryIfNeeded(c.fs.FS, yamlstore.DefaultPath); err != nil { return err } } if err := cfg.Load(); err != nil { return fmt.Errorf("error loading configuration: %w", err) } return c.getInfoFromConfig(cfg.Config()) } // isAlphaGenerateCommand checks if the command invocation is `kubebuilder alpha generate` // by scanning os.Args (excluding global flags). It returns true if "alpha" is followed by "generate". func isAlphaGenerateCommand(args []string) bool { positional := []string{} skip := false for i := range args { arg := args[i] // Skip flags and their values if strings.HasPrefix(arg, "-") { // If the flag is in --flag=value format, skip only this one if strings.Contains(arg, "=") { continue } // If it's --flag value format, skip next one too if i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { skip = true } continue } if skip { skip = false continue } positional = append(positional, arg) } // Check for `alpha generate` in positional arguments for i := 0; i < len(positional)-1; i++ { if positional[i] == "alpha" && positional[i+1] == "generate" { return true } } return false } // patchProjectFileInMemoryIfNeeded updates deprecated plugin keys in the PROJECT file in place, // so that users can run `kubebuilder alpha generate` even with older plugin layouts. // // See: https://github.com/kubernetes-sigs/kubebuilder/issues/4433 // // This ensures the CLI can successfully load the config without failing on unsupported plugin versions. func patchProjectFileInMemoryIfNeeded(fs afero.Fs, path string) error { type pluginReplacement struct { Old string New string } replacements := []pluginReplacement{ {"go.kubebuilder.io/v2", "go.kubebuilder.io/v4"}, {"go.kubebuilder.io/v3", "go.kubebuilder.io/v4"}, {"go.kubebuilder.io/v3-alpha", "go.kubebuilder.io/v4"}, } content, err := afero.ReadFile(fs, path) if err != nil { return nil } original := string(content) modified := original for _, rep := range replacements { if strings.Contains(modified, rep.Old) { modified = strings.ReplaceAll(modified, rep.Old, rep.New) log.Warn("Project is using an old and unsupported plugin layout", "old_layout", rep.Old, "new_layout", rep.New, "note", "Replace in memory to allow `alpha generate` to work.", ) } } if modified != original { err := afero.WriteFile(fs, path, []byte(modified), machinery.DefaultFilePermission) if err != nil { return fmt.Errorf("failed to write patched PROJECT file: %w", err) } } return nil } // getInfoFromConfig obtains the project version and plugin keys from the project config. // It is extracted from getInfoFromConfigFile for testing purposes. func (c *CLI) getInfoFromConfig(projectConfig config.Config) error { c.pluginKeys = projectConfig.GetPluginChain() c.projectVersion = projectConfig.GetVersion() for _, pluginKey := range c.pluginKeys { if err := plugin.ValidateKey(pluginKey); err != nil { return fmt.Errorf("invalid plugin key found in project configuration file: %w", err) } } return nil } // getInfoFromFlags obtains the project version and plugin keys from flags. func (c *CLI) getInfoFromFlags(hasConfigFile bool) error { // Check if --plugins is followed by --help or -h to avoid parsing help as a plugin value // This fixes: kubebuilder init --plugins --help for i := 0; i < len(os.Args)-1; i++ { if os.Args[i] == "--plugins" || os.Args[i] == "--plugins=" { nextArg := os.Args[i+1] if isHelpFlag(nextArg) { // Help was requested, return early to let Cobra handle it return nil } } } // Partially parse the command line arguments fs := pflag.NewFlagSet("base", pflag.ContinueOnError) // Load the base command global flags fs.AddFlagSet(c.cmd.PersistentFlags()) // If we were unable to load the project configuration, we should also accept the project version flag var projectVersionStr string if !hasConfigFile { fs.StringVar(&projectVersionStr, projectVersionFlag, "", "project version") } // FlagSet special cases --help and -h, so we need to create a dummy flag with these 2 values to prevent the default // behavior (printing the usage of this FlagSet) as we want to print the usage message of the underlying command. fs.BoolP("help", "h", false, fmt.Sprintf("help for %s", c.commandName)) // Omit unknown flags to avoid parsing errors fs.ParseErrorsAllowlist = pflag.ParseErrorsAllowlist{UnknownFlags: true} // Parse the arguments if err := fs.Parse(os.Args[1:]); err != nil { return fmt.Errorf("could not parse flags: %w", err) } // If any plugin key was provided, replace those from the project configuration file if pluginKeys, err := fs.GetStringSlice(pluginsFlag); err != nil { return fmt.Errorf("invalid flag %q: %w", pluginsFlag, err) } else if len(pluginKeys) != 0 { // Filter out help flags that may have been incorrectly parsed as plugin values // This fixes the issue where "kubebuilder edit --plugins --help" treats --help as a plugin validPluginKeys := make([]string, 0, len(pluginKeys)) helpRequested := false for _, key := range pluginKeys { key = strings.TrimSpace(key) // Skip help flags if isHelpFlag(key) { helpRequested = true continue } validPluginKeys = append(validPluginKeys, key) } // If help was requested via --plugins flag, set the help flag to trigger Cobra's help display // This prevents command execution and shows help instead if helpRequested { if err := fs.Set("help", "true"); err == nil { return nil } // If setting help flag fails, still return nil to avoid validation errors return nil } // Validate the remaining plugin keys for i, key := range validPluginKeys { if err := plugin.ValidateKey(key); err != nil { return fmt.Errorf("invalid plugin %q found in flags: %w", validPluginKeys[i], err) } } c.pluginKeys = validPluginKeys } // If the project version flag was accepted but not provided keep the empty version and try to resolve it later, // else validate the provided project version if projectVersionStr != "" { if err := c.projectVersion.Parse(projectVersionStr); err != nil { return fmt.Errorf("invalid project version flag: %w", err) } } return nil } // getInfoFromDefaults obtains the plugin keys, and maybe the project version from the default values func (c *CLI) getInfoFromDefaults() { // Should not use default values if a plugin was already set // This checks includes the case where a project configuration file was found, // as it will always have at least one plugin key set by now if len(c.pluginKeys) != 0 { // We don't assign a default value for project version here because we may be able to // resolve the project version after resolving the plugins. return } // If the user provided a project version, use the default plugins for that project version if c.projectVersion.Validate() == nil { c.pluginKeys = c.defaultPlugins[c.projectVersion] return } // Else try to use the default plugins for the default project version if c.defaultProjectVersion.Validate() == nil { var found bool if c.pluginKeys, found = c.defaultPlugins[c.defaultProjectVersion]; found { c.projectVersion = c.defaultProjectVersion return } } // Else check if only default plugins for a project version were provided if len(c.defaultPlugins) == 1 { for projectVersion, defaultPlugins := range c.defaultPlugins { c.pluginKeys = defaultPlugins c.projectVersion = projectVersion return } } } const unstablePluginMsg = " (plugin version is unstable, there may be an upgrade available: " + "https://kubebuilder.io/plugins/plugins-versioning)" // resolvePlugins selects from the available plugins those that match the project version and plugin keys provided. func (c *CLI) resolvePlugins() error { knownProjectVersion := c.projectVersion.Validate() == nil for _, pluginKey := range c.pluginKeys { var extraErrMsg string plugins := make([]plugin.Plugin, 0, len(c.plugins)) for _, p := range c.plugins { plugins = append(plugins, p) } // We can omit the error because plugin keys have already been validated plugins, _ = plugin.FilterPluginsByKey(plugins, pluginKey) if knownProjectVersion { plugins = plugin.FilterPluginsByProjectVersion(plugins, c.projectVersion) extraErrMsg += fmt.Sprintf(" for project version %q", c.projectVersion) } // Plugins are often released as "unstable" (alpha/beta) versions, then upgraded to "stable". // This upgrade effectively removes a plugin, which is fine because unstable plugins are // under no support contract. However users should be notified _why_ their plugin cannot be found. if _, version := plugin.SplitKey(pluginKey); version != "" { var ver plugin.Version if err := ver.Parse(version); err != nil { return fmt.Errorf("error parsing input plugin version from key %q: %w", pluginKey, err) } if !ver.IsStable() { extraErrMsg += unstablePluginMsg } } // Only 1 plugin can match switch len(plugins) { case 1: c.resolvedPlugins = append(c.resolvedPlugins, plugins[0]) case 0: return fmt.Errorf("no plugin could be resolved with key %q%s", pluginKey, extraErrMsg) default: return fmt.Errorf("ambiguous plugin %q%s", pluginKey, extraErrMsg) } } // Now we can try to resolve the project version if not known by this point if !knownProjectVersion && len(c.resolvedPlugins) > 0 { // Extract the common supported project versions supportedProjectVersions := plugin.CommonSupportedProjectVersions(c.resolvedPlugins...) // If there is only one common supported project version, resolve to it ProjectNumberVersionSwitch: switch len(supportedProjectVersions) { case 1: c.projectVersion = supportedProjectVersions[0] case 0: return fmt.Errorf("no project version supported by all the resolved plugins") default: supportedProjectVersionStrings := make([]string, 0, len(supportedProjectVersions)) for _, supportedProjectVersion := range supportedProjectVersions { // In case one of the multiple supported versions is the default one, choose that and exit the switch if supportedProjectVersion.Compare(c.defaultProjectVersion) == 0 { c.projectVersion = c.defaultProjectVersion break ProjectNumberVersionSwitch } supportedProjectVersionStrings = append(supportedProjectVersionStrings, fmt.Sprintf("%q", supportedProjectVersion)) } return fmt.Errorf("ambiguous project version, resolved plugins support the following project versions: %s", strings.Join(supportedProjectVersionStrings, ", ")) } } return nil } // addSubcommands returns a root command with a subcommand tree reflecting the // current project's state. func (c *CLI) addSubcommands() { // add the alpha command if it has any subcommands enabled c.addAlphaCmd() // kubebuilder completion // Only add completion if requested if c.completionCommand { c.cmd.AddCommand(c.newCompletionCmd()) } // kubebuilder create createCmd := c.newCreateCmd() // kubebuilder create api createCmd.AddCommand(c.newCreateAPICmd()) createCmd.AddCommand(c.newCreateWebhookCmd()) if createCmd.HasSubCommands() { c.cmd.AddCommand(createCmd) } // kubebuilder edit c.cmd.AddCommand(c.newEditCmd()) // kubebuilder init c.cmd.AddCommand(c.newInitCmd()) // kubebuilder version // Only add version if a version string was provided if c.version != "" { c.cmd.AddCommand(c.newVersionCmd()) } } // addExtraCommands adds the additional commands. func (c *CLI) addExtraCommands() error { for _, cmd := range c.extraCommands { for _, subCmd := range c.cmd.Commands() { if cmd.Name() == subCmd.Name() { return fmt.Errorf("command %q already exists", cmd.Name()) } } c.cmd.AddCommand(cmd) } return nil } // printDeprecationWarnings prints the deprecation warnings of the resolved plugins. func (c CLI) printDeprecationWarnings() { for _, p := range c.resolvedPlugins { if p != nil && p.(plugin.Deprecated) != nil && len(p.(plugin.Deprecated).DeprecationWarning()) > 0 { _, _ = fmt.Fprintf(os.Stderr, noticeColor, fmt.Sprintf(deprecationFmt, p.(plugin.Deprecated).DeprecationWarning())) } } } // metadata returns CLI's metadata. func (c CLI) metadata() plugin.CLIMetadata { return plugin.CLIMetadata{ CommandName: c.commandName, } } // Run executes the CLI utility. // // If an error is found, command help and examples will be printed. func (c CLI) Run() error { if err := c.cmd.Execute(); err != nil { // Don't return error if help was displayed (from --plugins --help pattern) if err == errHelpDisplayed { return nil } return fmt.Errorf("error executing command: %w", err) } return nil } // Command returns the underlying root command. func (c CLI) Command() *cobra.Command { return c.cmd } ================================================ FILE: pkg/cli/cli_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "fmt" "io" "os" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/afero" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4" ) func makeMockPluginsFor(projectVersion config.Version, pluginKeys ...string) []plugin.Plugin { plugins := make([]plugin.Plugin, 0, len(pluginKeys)) for _, key := range pluginKeys { n, v := plugin.SplitKey(key) plugins = append(plugins, newMockPlugin(n, v, projectVersion)) } return plugins } func makeMapFor(plugins ...plugin.Plugin) map[string]plugin.Plugin { pluginMap := make(map[string]plugin.Plugin, len(plugins)) for _, p := range plugins { pluginMap[plugin.KeyFor(p)] = p } return pluginMap } func setFlag(flag, value string) { os.Args = append(os.Args, "subcommand", "--"+flag, value) } func setBoolFlag(flag string) { os.Args = append(os.Args, "subcommand", "--"+flag) } func setProjectVersionFlag(value string) { setFlag(projectVersionFlag, value) } func setPluginsFlag(value string) { setFlag(pluginsFlag, value) } func hasSubCommand(cmd *cobra.Command, name string) bool { for _, subcommand := range cmd.Commands() { if subcommand.Name() == name { return true } } return false } type pluginChainCapturingSubcommand struct { pluginChain []string } func (s *pluginChainCapturingSubcommand) Scaffold(machinery.Filesystem) error { return nil } func (s *pluginChainCapturingSubcommand) SetPluginChain(chain []string) { s.pluginChain = append([]string(nil), chain...) } type testCreateAPIPlugin struct { name string version plugin.Version subcommand *testCreateAPISubcommand projectVers []config.Version } func newTestCreateAPIPlugin(name string, version plugin.Version) testCreateAPIPlugin { return testCreateAPIPlugin{ name: name, version: version, subcommand: &testCreateAPISubcommand{}, projectVers: []config.Version{{Number: 3}}, } } func (p testCreateAPIPlugin) Name() string { return p.name } func (p testCreateAPIPlugin) Version() plugin.Version { return p.version } func (p testCreateAPIPlugin) SupportedProjectVersions() []config.Version { return p.projectVers } func (p testCreateAPIPlugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return p.subcommand } type testCreateAPISubcommand struct{} func (s *testCreateAPISubcommand) InjectResource(*resource.Resource) error { return nil } func (s *testCreateAPISubcommand) Scaffold(machinery.Filesystem) error { return nil } type fakeStore struct { cfg config.Config } func (f *fakeStore) New(config.Version) error { return nil } func (f *fakeStore) Load() error { return nil } func (f *fakeStore) LoadFrom(string) error { return nil } func (f *fakeStore) Save() error { return nil } func (f *fakeStore) SaveTo(string) error { return nil } func (f *fakeStore) Config() config.Config { return f.cfg } type captureSubcommand struct { lastChain []string } func (c *captureSubcommand) Scaffold(machinery.Filesystem) error { return nil } var _ = Describe("CLI", func() { var ( c *CLI projectVersion config.Version ) BeforeEach(func() { c = &CLI{ fs: machinery.Filesystem{FS: afero.NewMemMapFs()}, } projectVersion = config.Version{Number: 3} }) Describe("filterSubcommands", func() { It("propagates bundle keys to wrapped subcommands", func() { bundleVersion := plugin.Version{Number: 1, Stage: stage.Alpha} fooPlugin := newTestCreateAPIPlugin("deploy-image.go.kubebuilder.io", plugin.Version{Number: 1, Stage: stage.Alpha}) barPlugin := newTestCreateAPIPlugin("deploy-image.go.kubebuilder.io", plugin.Version{Number: 1, Stage: stage.Alpha}) fooBundle, err := plugin.NewBundleWithOptions( plugin.WithName("deploy-image.foo.example.com"), plugin.WithVersion(bundleVersion), plugin.WithPlugins(fooPlugin), ) Expect(err).NotTo(HaveOccurred()) barBundle, err := plugin.NewBundleWithOptions( plugin.WithName("deploy-image.bar.example.com"), plugin.WithVersion(bundleVersion), plugin.WithPlugins(barPlugin), ) Expect(err).NotTo(HaveOccurred()) c.resolvedPlugins = []plugin.Plugin{fooBundle, barBundle} tuples := c.filterSubcommands( func(p plugin.Plugin) bool { _, isCreateAPI := p.(plugin.CreateAPI) return isCreateAPI }, func(p plugin.Plugin) plugin.Subcommand { return p.(plugin.CreateAPI).GetCreateAPISubcommand() }, ) Expect(tuples).To(HaveLen(2)) Expect(tuples[0].key).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha")) Expect(tuples[0].configKey).To(Equal("deploy-image.foo.example.com/v1-alpha")) Expect(tuples[1].key).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha")) Expect(tuples[1].configKey).To(Equal("deploy-image.bar.example.com/v1-alpha")) }) }) Describe("executionHooksFactory", func() { It("temporarily reorders the plugin chain while invoking bundled subcommands", func() { cfg := cfgv3.New() Expect(cfg.SetPluginChain([]string{ "deploy-image.foo.example.com/v1-alpha", "deploy-image.bar.example.com/v1-alpha", })).To(Succeed()) store := &fakeStore{cfg: cfg} first := &captureSubcommand{} second := &captureSubcommand{} factory := executionHooksFactory{ store: store, subcommands: []keySubcommandTuple{ {configKey: "deploy-image.foo.example.com/v1-alpha", subcommand: first}, {configKey: "deploy-image.bar.example.com/v1-alpha", subcommand: second}, }, errorMessage: "test", } callErr := factory.forEach(func(sub plugin.Subcommand) error { cs := sub.(*captureSubcommand) cs.lastChain = append([]string(nil), store.Config().GetPluginChain()...) return nil }, "scaffold") Expect(callErr).NotTo(HaveOccurred()) Expect(first.lastChain[0]).To(Equal("deploy-image.foo.example.com/v1-alpha")) Expect(second.lastChain[0]).To(Equal("deploy-image.bar.example.com/v1-alpha")) Expect(store.Config().GetPluginChain()).To(Equal([]string{ "deploy-image.foo.example.com/v1-alpha", "deploy-image.bar.example.com/v1-alpha", })) }) }) Context("buildCmd", func() { var projectFile string BeforeEach(func() { projectFile = `domain: zeusville.com layout: go.kubebuilder.io/v3 projectName: demo-zeus-operator repo: github.com/jmrodri/demo-zeus-operator resources: - crdVersion: v1 group: test kind: Test version: v1 version: 3-alpha plugins: manifests.sdk.operatorframework.io/v2: {} ` f, err := c.fs.FS.Create("PROJECT") Expect(err).To(Not(HaveOccurred())) _, err = f.WriteString(projectFile) Expect(err).To(Not(HaveOccurred())) }) When("reading a 3-alpha config", func() { It("should succeed and set the projectVersion", func() { err := c.buildCmd() Expect(err).To(Not(HaveOccurred())) Expect(c.projectVersion.Compare( config.Version{ Number: 3, Stage: stage.Stable, })).To(Equal(0)) }) It("should fail when stable is not registered ", func() { // overwrite project file with fake 4-alpha f, err := c.fs.FS.OpenFile("PROJECT", os.O_WRONLY, 0) Expect(err).To(Not(HaveOccurred())) _, err = f.WriteString(strings.ReplaceAll(projectFile, "3-alpha", "4-alpha")) Expect(err).To(Not(HaveOccurred())) // buildCmd should return an error err = c.buildCmd() Expect(err).To(HaveOccurred()) }) }) }) // TODO: test CLI.getInfoFromConfigFile using a mock filesystem Context("getInfoFromConfig", func() { When("having a single plugin in the layout field", func() { It("should succeed", func() { pluginChain := []string{"go.kubebuilder.io/v4"} projectConfig := cfgv3.New() Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginChain)) Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) When("having multiple plugins in the layout field", func() { It("should succeed", func() { pluginChain := []string{"go.kubebuilder.io/v2", "deploy-image.go.kubebuilder.io/v1-alpha"} projectConfig := cfgv3.New() Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) Expect(c.getInfoFromConfig(projectConfig)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginChain)) Expect(c.projectVersion.Compare(projectConfig.GetVersion())).To(Equal(0)) }) }) When("having invalid plugin keys in the layout field", func() { It("should fail", func() { pluginChain := []string{"_/v1"} projectConfig := cfgv3.New() Expect(projectConfig.SetPluginChain(pluginChain)).To(Succeed()) Expect(c.getInfoFromConfig(projectConfig)).NotTo(Succeed()) }) }) }) Context("getInfoFromFlags", func() { // Save os.Args and restore it for every test var args []string BeforeEach(func() { c.cmd = c.newRootCmd() args = os.Args }) AfterEach(func() { os.Args = args }) When("no flag is set", func() { It("should succeed", func() { Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(BeEmpty()) Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) }) When(fmt.Sprintf("--%s flag is set", pluginsFlag), func() { It("should succeed using one plugin key", func() { pluginKeys := []string{"go/v1"} setPluginsFlag(strings.Join(pluginKeys, ",")) Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) It("should succeed using more than one plugin key", func() { pluginKeys := []string{"go/v1", "example/v2", "test/v1"} setPluginsFlag(strings.Join(pluginKeys, ",")) Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) It("should succeed using more than one plugin key with spaces", func() { pluginKeys := []string{"go/v1", "example/v2", "test/v1"} setPluginsFlag(strings.Join(pluginKeys, ", ")) Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) It("should fail for an invalid plugin key", func() { setPluginsFlag("_/v1") Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) }) }) When(fmt.Sprintf("--%s flag is set", projectVersionFlag), func() { It("should succeed", func() { setProjectVersionFlag(projectVersion.String()) Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(BeEmpty()) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) It("should fail for an invalid project version", func() { setProjectVersionFlag("v_1") Expect(c.getInfoFromFlags(false)).NotTo(Succeed()) }) }) When(fmt.Sprintf("--%s and --%s flags are set", pluginsFlag, projectVersionFlag), func() { It("should succeed using one plugin key", func() { pluginKeys := []string{"go/v1"} setPluginsFlag(strings.Join(pluginKeys, ",")) setProjectVersionFlag(projectVersion.String()) Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) It("should succeed using more than one plugin key", func() { pluginKeys := []string{"go/v1", "example/v2", "test/v1"} setPluginsFlag(strings.Join(pluginKeys, ",")) setProjectVersionFlag(projectVersion.String()) Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) It("should succeed using more than one plugin key with spaces", func() { pluginKeys := []string{"go/v1", "example/v2", "test/v1"} setPluginsFlag(strings.Join(pluginKeys, ", ")) setProjectVersionFlag(projectVersion.String()) Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) }) When("additional flags are set", func() { It("should succeed", func() { setFlag("extra-flag", "extra-value") Expect(c.getInfoFromFlags(false)).To(Succeed()) }) // `--help` is not captured by the allowlist, so we need to special case it It("should not fail for `--help`", func() { setBoolFlag("help") Expect(c.getInfoFromFlags(false)).To(Succeed()) }) // When --plugins is followed by --help, --help is consumed as plugin value // This should not trigger plugin validation errors It("should not fail when `--plugins --help` is used together", func() { os.Args = append(os.Args, "edit", "--plugins", "--help") Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(BeEmpty()) }) // Same test for short help flag It("should not fail when `--plugins -h` is used together", func() { os.Args = append(os.Args, "edit", "--plugins", "-h") Expect(c.getInfoFromFlags(false)).To(Succeed()) Expect(c.pluginKeys).To(BeEmpty()) }) }) }) Context("getInfoFromDefaults", func() { var pluginKeys []string BeforeEach(func() { pluginKeys = []string{"go.kubebuilder.io/v2"} }) It("should be a no-op if already have plugin keys", func() { c.pluginKeys = pluginKeys c.getInfoFromDefaults() Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(config.Version{})).To(Equal(0)) }) It("should succeed if default plugins for project version are set", func() { c.projectVersion = projectVersion c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} c.getInfoFromDefaults() Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) It("should succeed if default plugins for default project version are set", func() { c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} c.defaultProjectVersion = projectVersion c.getInfoFromDefaults() Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) It("should succeed if default plugins for only a single project version are set", func() { c.defaultPlugins = map[config.Version][]string{projectVersion: pluginKeys} c.getInfoFromDefaults() Expect(c.pluginKeys).To(Equal(pluginKeys)) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) }) Context("resolvePlugins", func() { BeforeEach(func() { pluginKeys := []string{ "foo.example.com/v1", "bar.example.com/v1", "baz.example.com/v1", "foo.kubebuilder.io/v1", "foo.kubebuilder.io/v2", "bar.kubebuilder.io/v1", "bar.kubebuilder.io/v2", } plugins := makeMockPluginsFor(projectVersion, pluginKeys...) plugins = append(plugins, newMockPlugin("invalid.kubebuilder.io", "v1"), newMockPlugin("only1.kubebuilder.io", "v1", config.Version{Number: 1}), newMockPlugin("only2.kubebuilder.io", "v1", config.Version{Number: 2}), newMockPlugin("1and2.kubebuilder.io", "v1", config.Version{Number: 1}, config.Version{Number: 2}), newMockPlugin("2and3.kubebuilder.io", "v1", config.Version{Number: 2}, config.Version{Number: 3}), newMockPlugin("1-2and3.kubebuilder.io", "v1", config.Version{Number: 1}, config.Version{Number: 2}, config.Version{Number: 3}), ) pluginMap := makeMapFor(plugins...) c.plugins = pluginMap }) DescribeTable("should resolve", func(key, qualified string) { c.pluginKeys = []string{key} c.projectVersion = projectVersion Expect(c.resolvePlugins()).To(Succeed()) Expect(c.resolvedPlugins).To(HaveLen(1)) Expect(plugin.KeyFor(c.resolvedPlugins[0])).To(Equal(qualified)) }, Entry("fully qualified plugin", "foo.example.com/v1", "foo.example.com/v1"), Entry("plugin without version", "foo.example.com", "foo.example.com/v1"), Entry("shortname without version", "baz", "baz.example.com/v1"), Entry("shortname with version", "foo/v2", "foo.kubebuilder.io/v2"), ) DescribeTable("should not resolve", func(key string) { c.pluginKeys = []string{key} c.projectVersion = projectVersion Expect(c.resolvePlugins()).NotTo(Succeed()) }, Entry("for an ambiguous version", "foo.kubebuilder.io"), Entry("for an ambiguous name", "foo/v1"), Entry("for an ambiguous name and version", "foo"), Entry("for a non-existent name", "blah"), Entry("for a non-existent version", "foo.example.com/v2"), Entry("for a non-existent version", "foo/v3"), Entry("for a non-existent version", "foo.example.com/v3"), Entry("for a plugin that doesn't support the project version", "invalid.kubebuilder.io/v1"), ) It("should succeed if only one common project version is found", func() { c.pluginKeys = []string{"1and2", "2and3"} Expect(c.resolvePlugins()).To(Succeed()) Expect(c.projectVersion.Compare(config.Version{Number: 2})).To(Equal(0)) }) It("should fail if no common project version is found", func() { c.pluginKeys = []string{"only1", "only2"} Expect(c.resolvePlugins()).NotTo(Succeed()) }) It("should fail if more than one common project versions are found", func() { c.pluginKeys = []string{"1and2", "1-2and3"} Expect(c.resolvePlugins()).NotTo(Succeed()) }) It("should succeed if more than one common project versions are found and one is the default", func() { c.pluginKeys = []string{"2and3", "1-2and3"} c.defaultProjectVersion = projectVersion Expect(c.resolvePlugins()).To(Succeed()) Expect(c.projectVersion.Compare(projectVersion)).To(Equal(0)) }) }) Context("applySubcommandHooks", func() { var ( cmd *cobra.Command sub1, sub2 *pluginChainCapturingSubcommand tuples []keySubcommandTuple chainKeys []string ) BeforeEach(func() { cmd = &cobra.Command{} sub1 = &pluginChainCapturingSubcommand{} sub2 = &pluginChainCapturingSubcommand{} tuples = []keySubcommandTuple{ {key: "alpha.kubebuilder.io/v1", subcommand: sub1}, {key: "beta.kubebuilder.io/v1", subcommand: sub2}, } chainKeys = []string{"alpha.kubebuilder.io/v1", "beta.kubebuilder.io/v1"} }) It("sets the plugin chain on subcommands", func() { c.applySubcommandHooks(cmd, tuples, "test", false) Expect(sub1.pluginChain).To(Equal(chainKeys)) Expect(sub2.pluginChain).To(Equal(chainKeys)) }) It("sets the plugin chain when creating a new configuration", func() { c.resolvedPlugins = makeMockPluginsFor(projectVersion, chainKeys...) c.applySubcommandHooks(cmd, tuples, "test", true) Expect(sub1.pluginChain).To(Equal(chainKeys)) Expect(sub2.pluginChain).To(Equal(chainKeys)) }) }) Context("New", func() { var c *CLI var err error When("no option is provided", func() { It("should create a valid CLI", func() { _, err = New() Expect(err).NotTo(HaveOccurred()) }) }) // NOTE: Options are extensively tested in their own tests. // The ones tested here ensure better coverage. When("providing a version string", func() { It("should create a valid CLI", func() { const version = "version string" c, err = New( WithPlugins(&golangv4.Plugin{}), WithDefaultPlugins(projectVersion, &golangv4.Plugin{}), WithVersion(version), ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c.cmd, "version")).To(BeTrue()) // Test the version command c.cmd.SetArgs([]string{"version"}) // Overwrite stdout to read the output and reset it afterwards r, w, _ := os.Pipe() temp := os.Stdout defer func() { os.Stdout = temp }() os.Stdout = w Expect(c.cmd.Execute()).Should(Succeed()) _ = w.Close() Expect(err).NotTo(HaveOccurred()) printed, _ := io.ReadAll(r) Expect(string(printed)).To(Equal( fmt.Sprintf("%s\n", version))) }) }) When("enabling completion", func() { It("should create a valid CLI", func() { c, err = New( WithPlugins(&golangv4.Plugin{}), WithDefaultPlugins(projectVersion, &golangv4.Plugin{}), WithCompletion(), ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c.cmd, "completion")).To(BeTrue()) }) }) When("providing an invalid option", func() { It("should return an error", func() { // An empty project version is not valid _, err = New(WithDefaultProjectVersion(config.Version{})) Expect(err).To(HaveOccurred()) }) }) When("being unable to resolve plugins", func() { // Save os.Args and restore it for every test var args []string BeforeEach(func() { args = os.Args }) AfterEach(func() { os.Args = args }) It("should return a CLI that returns an error", func() { setPluginsFlag("foo") c, err = New() Expect(err).NotTo(HaveOccurred()) // Overwrite stderr to read the output and reset it afterwards _, w, _ := os.Pipe() temp := os.Stderr defer func() { os.Stderr = temp _ = w.Close() }() os.Stderr = w Expect(c.Run()).NotTo(Succeed()) }) }) When("providing extra commands", func() { It("should create a valid CLI for non-conflicting ones", func() { extraCommand := &cobra.Command{Use: "extra"} c, err = New( WithPlugins(&golangv4.Plugin{}), WithDefaultPlugins(projectVersion, &golangv4.Plugin{}), WithExtraCommands(extraCommand), ) Expect(err).NotTo(HaveOccurred()) Expect(hasSubCommand(c.cmd, extraCommand.Use)).To(BeTrue()) }) It("should return an error for conflicting ones", func() { extraCommand := &cobra.Command{Use: "init"} c, err = New( WithPlugins(&golangv4.Plugin{}), WithDefaultPlugins(projectVersion, &golangv4.Plugin{}), WithExtraCommands(extraCommand), ) Expect(err).To(HaveOccurred()) }) }) When("providing extra alpha commands", func() { It("should create a valid CLI for non-conflicting ones", func() { extraAlphaCommand := &cobra.Command{Use: "extra"} c, err = New( WithPlugins(&golangv4.Plugin{}), WithDefaultPlugins(projectVersion, &golangv4.Plugin{}), WithExtraAlphaCommands(extraAlphaCommand), ) Expect(err).NotTo(HaveOccurred()) var alpha *cobra.Command for _, subcmd := range c.cmd.Commands() { if subcmd.Name() == alphaCommand { alpha = subcmd break } } Expect(alpha).NotTo(BeNil()) Expect(hasSubCommand(alpha, extraAlphaCommand.Use)).To(BeTrue()) }) It("should return an error for conflicting ones", func() { extraAlphaCommand := &cobra.Command{Use: "extra"} _, err = New( WithPlugins(&golangv4.Plugin{}), WithDefaultPlugins(projectVersion, &golangv4.Plugin{}), WithExtraAlphaCommands(extraAlphaCommand, extraAlphaCommand), ) Expect(err).To(HaveOccurred()) }) }) When("providing deprecated plugins", func() { It("should succeed and print the deprecation notice", func() { const ( deprecationWarning = "DEPRECATED" ) deprecatedPlugin := newMockDeprecatedPlugin("deprecated", "v1", deprecationWarning, projectVersion) // Overwrite stderr to read the deprecation output and reset it afterwards r, w, _ := os.Pipe() temp := os.Stderr defer func() { os.Stderr = temp }() os.Stderr = w c, err = New( WithPlugins(deprecatedPlugin), WithDefaultPlugins(projectVersion, deprecatedPlugin), WithDefaultProjectVersion(projectVersion), ) _ = w.Close() Expect(err).NotTo(HaveOccurred()) printed, _ := io.ReadAll(r) Expect(string(printed)).To(Equal( fmt.Sprintf(noticeColor, fmt.Sprintf(deprecationFmt, deprecationWarning)))) }) }) When("new succeeds", func() { It("should return the underlying command", func() { c, err = New() Expect(err).NotTo(HaveOccurred()) Expect(c.Command()).NotTo(BeNil()) Expect(c.Command()).To(Equal(c.cmd)) }) }) }) }) ================================================ FILE: pkg/cli/cmd_helpers.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "errors" "fmt" "os" "strings" "github.com/spf13/cobra" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" yamlstore "sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) // noResolvedPluginError is returned by subcommands that require a plugin when none was resolved. type noResolvedPluginError struct{} // Error implements error interface. func (e noResolvedPluginError) Error() string { return "no resolved plugin, please verify the project version and plugins specified in flags or configuration file" } // noAvailablePluginError is returned by subcommands that require a plugin when none of their specific type was found. type noAvailablePluginError struct { subcommand string } // Error implements error interface. func (e noAvailablePluginError) Error() string { return fmt.Sprintf("resolved plugins do not provide any %s subcommand", e.subcommand) } // cmdErr updates a cobra command to output error information when executed // or used with the help flag. func cmdErr(cmd *cobra.Command, err error) { cmd.Long = fmt.Sprintf("%s\nNote: %v", cmd.Long, err) cmd.RunE = errCmdFunc(err) } // errCmdFunc returns a cobra RunE function that returns the provided error func errCmdFunc(err error) func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { return err } } // keySubcommandTuple pairs a plugin key with its subcommand. // key is the plugin's own key, configKey is the bundle key (if wrapped in a bundle). type keySubcommandTuple struct { key string configKey string subcommand plugin.Subcommand // skip marks subcommands that should be skipped after a plugin.ExitError. skip bool } type pluginChainSetter interface { SetPluginChain([]string) } // filterSubcommands returns plugin keys and subcommands from resolved plugins. func (c *CLI) filterSubcommands( filter func(plugin.Plugin) bool, extract func(plugin.Plugin) plugin.Subcommand, ) []keySubcommandTuple { tuples := make([]keySubcommandTuple, 0, len(c.resolvedPlugins)) for _, p := range c.resolvedPlugins { tuples = append(tuples, collectSubcommands(p, plugin.KeyFor(p), filter, extract)...) } return tuples } func collectSubcommands( p plugin.Plugin, configKey string, filter func(plugin.Plugin) bool, extract func(plugin.Plugin) plugin.Subcommand, ) []keySubcommandTuple { if bundle, isBundle := p.(plugin.Bundle); isBundle { collected := make([]keySubcommandTuple, 0, len(bundle.Plugins())) for _, nested := range bundle.Plugins() { collected = append(collected, collectSubcommands(nested, configKey, filter, extract)...) } return collected } if !filter(p) { return nil } return []keySubcommandTuple{{ key: plugin.KeyFor(p), configKey: configKey, subcommand: extract(p), }} } // applySubcommandHooks runs the initialization hooks and wires pre-run, run, and post-run for the command. // Used by init, create api, create webhook, and edit. When several plugins define the same flag, // one flag is shown and its value is synced to all plugins after parse. func (c *CLI) applySubcommandHooks( cmd *cobra.Command, subcommands []keySubcommandTuple, errorMessage string, createConfig bool, ) { commandPluginChain := make([]string, len(subcommands)) for i, tuple := range subcommands { commandPluginChain[i] = tuple.key } for _, tuple := range subcommands { if setter, ok := tuple.subcommand.(pluginChainSetter); ok { setter.SetPluginChain(commandPluginChain) } } // In case we create a new project configuration we need to compute the plugin chain. pluginChain := make([]string, 0, len(c.resolvedPlugins)) if createConfig { // We extract the plugin keys again instead of using the ones obtained when filtering subcommands // as these plugins are unbundled but we want to keep bundle names in the plugin chain. for _, p := range c.resolvedPlugins { pluginChain = append(pluginChain, plugin.KeyFor(p)) } } result, err := initializationHooks(cmd, subcommands, c.metadata()) if err != nil { cmdErr(cmd, err) return } factory := executionHooksFactory{ fs: c.fs, store: yamlstore.New(c.fs), subcommands: subcommands, errorMessage: errorMessage, projectVersion: c.projectVersion, pluginChain: pluginChain, cliVersion: c.cliVersion, duplicateFlagValues: result.duplicateFlagValues, } cmd.PreRunE = factory.preRunEFunc(result.options, createConfig) cmd.RunE = factory.runEFunc() cmd.PostRunE = factory.postRunEFunc() } // appendPluginTable appends a filtered plugin table to the command's Long description. // For subcommands, it excludes the default scaffold and its component plugins. func (c *CLI) appendPluginTable(cmd *cobra.Command, filter func(plugin.Plugin) bool, title string) { pluginTable := c.getPluginTableFilteredForSubcommand(filter) cmd.Long = fmt.Sprintf("%s\n%s:\n\n%s\n", cmd.Long, title, pluginTable) } // initHooksResult holds the result of initializationHooks: resource options and // duplicate-flag values to sync after parse. type initHooksResult struct { options *resourceOptions duplicateFlagValues map[string][]pflag.Value } // mergeFlagSetInto merges flags from src into dest using AddFlagSet. If a flag name already exists, // the flag is not added again; its Value is stored in duplicateValues for later sync and the existing // Usage is extended. Returns an error if the same flag name is used with a different value type. func mergeFlagSetInto( dest *pflag.FlagSet, src *pflag.FlagSet, duplicateValues map[string][]pflag.Value, pluginKey string, firstPluginByFlag map[string]string, ) error { destNames := make(map[string]struct{}) dest.VisitAll(func(f *pflag.Flag) { destNames[f.Name] = struct{}{} }) dest.AddFlagSet(src) var err error src.VisitAll(func(flag *pflag.Flag) { if err != nil { return } existing := dest.Lookup(flag.Name) if _, wasInDest := destNames[flag.Name]; !wasInDest { firstPluginByFlag[flag.Name] = pluginKey existing.Usage = "For plugin (" + pluginKey + "): " + strings.TrimSpace(flag.Usage) return } if existing.Value.Type() != flag.Value.Type() { firstKey := firstPluginByFlag[flag.Name] err = fmt.Errorf( "plugins %q and %q use the same flag name %q but expect different value types: one %s, other %s", firstKey, pluginKey, flag.Name, existing.Value.Type(), flag.Value.Type(), ) return } duplicateValues[flag.Name] = append(duplicateValues[flag.Name], flag.Value) existing.Usage += " AND for plugin (" + pluginKey + "): " + strings.TrimSpace(flag.Usage) }) return err } // syncDuplicateFlags copies the parsed value of each flag to all duplicate Values from merge. // Call after the command has parsed flags (e.g. at the start of PreRunE). func syncDuplicateFlags(flags *pflag.FlagSet, duplicateValues map[string][]pflag.Value) { for name, values := range duplicateValues { parsed := flags.Lookup(name) if parsed == nil { continue } srcVal := parsed.Value.String() for _, v := range values { _ = v.Set(srcVal) } } } // initializationHooks runs update-metadata and bind-flags hooks. When multiple plugins bind the same // flag, one flag is used and its value is synced to all after parse; usage text is aggregated. // Returns an error if the same flag name is used with different value types (e.g. bool vs string). func initializationHooks( cmd *cobra.Command, subcommands []keySubcommandTuple, meta plugin.CLIMetadata, ) (*initHooksResult, error) { // Update metadata hook. subcmdMeta := plugin.SubcommandMetadata{ Description: cmd.Long, Examples: cmd.Example, } for _, tuple := range subcommands { if subcommand, updatesMetadata := tuple.subcommand.(plugin.UpdatesMetadata); updatesMetadata { subcommand.UpdateMetadata(meta, &subcmdMeta) } } cmd.Long = subcmdMeta.Description cmd.Example = subcmdMeta.Examples // Before binding specific plugin flags, bind common ones. requiresResource := false for _, tuple := range subcommands { if _, requiresResource = tuple.subcommand.(plugin.RequiresResource); requiresResource { break } } var options *resourceOptions if requiresResource { options = bindResourceFlags(cmd.Flags()) } // Bind flags hook: each plugin binds to a temporary FlagSet, then we merge into the command so // duplicate names do not panic; values are synced after parse and help text is aggregated. duplicateValues := make(map[string][]pflag.Value) firstPluginByFlag := make(map[string]string) for _, tuple := range subcommands { if subcommand, hasFlags := tuple.subcommand.(plugin.HasFlags); hasFlags { tmpSet := pflag.NewFlagSet(cmd.Name(), pflag.ExitOnError) subcommand.BindFlags(tmpSet) if err := mergeFlagSetInto(cmd.Flags(), tmpSet, duplicateValues, tuple.key, firstPluginByFlag); err != nil { return nil, err } } } return &initHooksResult{options: options, duplicateFlagValues: duplicateValues}, nil } type executionHooksFactory struct { // fs is the filesystem abstraction to scaffold files to. fs machinery.Filesystem // store is the backend used to load/save the project configuration. store store.Store // subcommands are the tuples representing the set of subcommands provided by the resolved plugins. subcommands []keySubcommandTuple // errorMessage is prepended to returned errors. errorMessage string // projectVersion is the project version that will be used to create new project configurations. // It is only used for initialization. projectVersion config.Version // pluginChain is the plugin chain configured for this project. pluginChain []string // cliVersion is the version of the CLI. cliVersion string // duplicateFlagValues maps flag names to Values to sync from the parsed flag in PreRunE. duplicateFlagValues map[string][]pflag.Value } func (factory *executionHooksFactory) forEach(cb func(subcommand plugin.Subcommand) error, errorMessage string) error { for i, tuple := range factory.subcommands { if tuple.skip { continue } err := factory.withPluginChain(tuple, func() error { return cb(tuple.subcommand) }) var exitError plugin.ExitError switch { case err == nil: // No error do nothing case errors.As(err, &exitError): // Exit errors imply that no further hooks of this subcommand should be called, so we flag it to be skipped factory.subcommands[i].skip = true fmt.Printf("skipping remaining hooks of %q: %s\n", tuple.key, exitError.Reason) default: // Any other error, wrap it return fmt.Errorf("%s: %s %q: %w", factory.errorMessage, errorMessage, tuple.key, err) } } return nil } func (factory *executionHooksFactory) withPluginChain(tuple keySubcommandTuple, cb func() error) (err error) { if tuple.configKey == "" { return cb() } cfg := factory.store.Config() if cfg == nil { return cb() } // Temporarily move configKey to the front so GetPluginKeyForConfig finds it first. // This ensures each bundled plugin saves config under the right key. original := append([]string(nil), cfg.GetPluginChain()...) newChain := moveKeyToFront(original, tuple.configKey) changed := !equalStringSlices(original, newChain) if changed { if setErr := cfg.SetPluginChain(newChain); setErr != nil { return fmt.Errorf("failed to set plugin chain for %q: %w", tuple.configKey, setErr) } defer func() { if resetErr := cfg.SetPluginChain(original); resetErr != nil && err == nil { err = fmt.Errorf("failed to reset plugin chain: %w", resetErr) } }() } return cb() } func moveKeyToFront(chain []string, key string) []string { if len(chain) == 0 { return []string{key} } if chain[0] == key { return chain } newChain := make([]string, 0, len(chain)+1) newChain = append(newChain, key) for _, existing := range chain { if existing == key { continue } newChain = append(newChain, existing) } return newChain } func equalStringSlices(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } // preRunEFunc returns a cobra RunE function that loads the configuration, creates the resource, // and executes inject config, inject resource, and pre-scaffold hooks. func (factory *executionHooksFactory) preRunEFunc( options *resourceOptions, createConfig bool, ) func(*cobra.Command, []string) error { return func(cmd *cobra.Command, _ []string) error { if len(factory.duplicateFlagValues) > 0 { syncDuplicateFlags(cmd.Flags(), factory.duplicateFlagValues) } if createConfig { // Check if a project configuration is already present. if err := factory.store.Load(); err == nil || !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("%s: already initialized", factory.errorMessage) } // Initialize the project configuration. if err := factory.store.New(factory.projectVersion); err != nil { return fmt.Errorf("%s: error initializing project configuration: %w", factory.errorMessage, err) } } else { // Load the project configuration. if err := factory.store.Load(); os.IsNotExist(err) { return fmt.Errorf("%s: failed to find configuration file, project must be initialized", factory.errorMessage) } else if err != nil { return fmt.Errorf("%s: failed to load configuration file: %w", factory.errorMessage, err) } } cfg := factory.store.Config() // Set the CLI version if creating a new project configuration. if createConfig { _ = cfg.SetCliVersion(factory.cliVersion) } // Set the pluginChain field. if len(factory.pluginChain) != 0 { _ = cfg.SetPluginChain(factory.pluginChain) } // Create the resource if non-nil options provided var res *resource.Resource if options != nil { // TODO: offer a flag instead of hard-coding project-wide domain options.Domain = cfg.GetDomain() if err := options.validate(); err != nil { return fmt.Errorf("%s: failed to create resource: %w", factory.errorMessage, err) } res = options.newResource() } // Inject config hook. if err := factory.forEach(func(subcommand plugin.Subcommand) error { if subcommand, requiresConfig := subcommand.(plugin.RequiresConfig); requiresConfig { return subcommand.InjectConfig(cfg) } return nil }, "unable to inject the configuration to"); err != nil { return err } if res != nil { // Inject resource hook. if err := factory.forEach(func(subcommand plugin.Subcommand) error { if subcommand, requiresResource := subcommand.(plugin.RequiresResource); requiresResource { return subcommand.InjectResource(res) } return nil }, "unable to inject the resource to"); err != nil { return err } if err := res.Validate(); err != nil { return fmt.Errorf("%s: created invalid resource: %w", factory.errorMessage, err) } } // Pre-scaffold hook. //nolint:revive if err := factory.forEach(func(subcommand plugin.Subcommand) error { if subcommand, hasPreScaffold := subcommand.(plugin.HasPreScaffold); hasPreScaffold { return subcommand.PreScaffold(factory.fs) } return nil }, "unable to run pre-scaffold tasks of"); err != nil { return err } return nil } } // runEFunc returns a cobra RunE function that executes the scaffold hook. func (factory *executionHooksFactory) runEFunc() func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { // Scaffold hook. //nolint:revive if err := factory.forEach(func(subcommand plugin.Subcommand) error { return subcommand.Scaffold(factory.fs) }, "unable to scaffold with"); err != nil { return err } return nil } } // postRunEFunc returns a cobra RunE function that saves the configuration // and executes the post-scaffold hook. func (factory *executionHooksFactory) postRunEFunc() func(*cobra.Command, []string) error { return func(*cobra.Command, []string) error { if err := factory.store.Save(); err != nil { return fmt.Errorf("%s: failed to save configuration file: %w", factory.errorMessage, err) } // Post-scaffold hook. //nolint:revive if err := factory.forEach(func(subcommand plugin.Subcommand) error { if subcommand, hasPostScaffold := subcommand.(plugin.HasPostScaffold); hasPostScaffold { return subcommand.PostScaffold() } return nil }, "unable to run post-scaffold tasks of"); err != nil { return err } return nil } } ================================================ FILE: pkg/cli/cmd_helpers_test.go ================================================ /* Copyright 2025 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 cli import ( "errors" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/cobra" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) var _ = Describe("cmd_helpers", func() { Context("error types", func() { It("noResolvedPluginError should return correct message", func() { err := noResolvedPluginError{} Expect(err.Error()).To(ContainSubstring("no resolved plugin")) Expect(err.Error()).To(ContainSubstring("verify the project version and plugins")) }) It("noAvailablePluginError should return correct message with subcommand", func() { err := noAvailablePluginError{subcommand: "init"} Expect(err.Error()).To(ContainSubstring("init")) Expect(err.Error()).To(ContainSubstring("do not provide any")) }) }) Context("cmdErr", func() { It("should update command with error information", func() { cmd := &cobra.Command{ Long: "Original description", RunE: func(*cobra.Command, []string) error { return nil }, } testError := errors.New("test error") cmdErr(cmd, testError) Expect(cmd.Long).To(ContainSubstring("Original description")) Expect(cmd.Long).To(ContainSubstring("test error")) Expect(cmd.RunE).NotTo(BeNil()) err := cmd.RunE(cmd, []string{}) Expect(err).To(Equal(testError)) }) }) Context("errCmdFunc", func() { It("should return a function that returns the provided error", func() { testError := errors.New("test error") runE := errCmdFunc(testError) err := runE(nil, nil) Expect(err).To(Equal(testError)) }) }) Context("moveKeyToFront", func() { It("should handle empty chain", func() { result := moveKeyToFront([]string{}, "key1") Expect(result).To(Equal([]string{"key1"})) }) It("should not change chain when key is already at front", func() { chain := []string{"key1", "key2", "key3"} result := moveKeyToFront(chain, "key1") Expect(result).To(Equal(chain)) }) It("should move key to front when it exists in chain", func() { chain := []string{"key1", "key2", "key3"} result := moveKeyToFront(chain, "key2") Expect(result).To(Equal([]string{"key2", "key1", "key3"})) }) It("should move key to front from end of chain", func() { chain := []string{"key1", "key2", "key3"} result := moveKeyToFront(chain, "key3") Expect(result).To(Equal([]string{"key3", "key1", "key2"})) }) It("should add key to front when not in chain", func() { chain := []string{"key1", "key2"} result := moveKeyToFront(chain, "key3") Expect(result).To(Equal([]string{"key3", "key1", "key2"})) }) It("should remove duplicate when moving key to front", func() { chain := []string{"key1", "key2", "key2"} result := moveKeyToFront(chain, "key2") Expect(result).To(Equal([]string{"key2", "key1"})) }) }) Context("equalStringSlices", func() { It("should return true for equal slices", func() { a := []string{"a", "b", "c"} b := []string{"a", "b", "c"} Expect(equalStringSlices(a, b)).To(BeTrue()) }) It("should return true for empty slices", func() { Expect(equalStringSlices([]string{}, []string{})).To(BeTrue()) }) It("should return false for different lengths", func() { a := []string{"a", "b"} b := []string{"a", "b", "c"} Expect(equalStringSlices(a, b)).To(BeFalse()) }) It("should return false for different content", func() { a := []string{"a", "b", "c"} b := []string{"a", "x", "c"} Expect(equalStringSlices(a, b)).To(BeFalse()) }) It("should return false for different order", func() { a := []string{"a", "b", "c"} b := []string{"c", "b", "a"} Expect(equalStringSlices(a, b)).To(BeFalse()) }) It("should handle nil slices", func() { var a, b []string Expect(equalStringSlices(a, b)).To(BeTrue()) }) }) Context("collectSubcommands", func() { var ( testPlugin *mockPluginWithSubcommand testSubcommand *mockTestSubcommand testBundle *mockPluginBundle testNestedPlugin *mockPluginWithSubcommand ) BeforeEach(func() { testSubcommand = &mockTestSubcommand{} testPlugin = newMockPluginWithSubcommand( "test.plugin", []config.Version{{Number: 1}}, testSubcommand) testNestedPlugin = newMockPluginWithSubcommand( "nested.plugin", []config.Version{{Number: 1}}, &mockTestSubcommand{}) testBundle = newMockPluginBundle( "test.bundle", []config.Version{{Number: 1}}, []plugin.Plugin{testNestedPlugin}) }) It("should return nil when filter returns false", func() { filter := func(plugin.Plugin) bool { return false } extract := func(plugin.Plugin) plugin.Subcommand { return testSubcommand } result := collectSubcommands(testPlugin, "config.key", filter, extract) Expect(result).To(BeNil()) }) It("should collect subcommand from single plugin", func() { filter := func(plugin.Plugin) bool { return true } extract := func(p plugin.Plugin) plugin.Subcommand { if mp, ok := p.(*mockPluginWithSubcommand); ok { return mp.subcommand } return nil } result := collectSubcommands(testPlugin, "config.key", filter, extract) Expect(result).To(HaveLen(1)) Expect(result[0].key).To(Equal("test.plugin/v1")) Expect(result[0].configKey).To(Equal("config.key")) Expect(result[0].subcommand).To(Equal(testSubcommand)) }) It("should collect subcommands from bundle", func() { filter := func(plugin.Plugin) bool { return true } extract := func(p plugin.Plugin) plugin.Subcommand { if mp, ok := p.(*mockPluginWithSubcommand); ok { return mp.subcommand } return nil } result := collectSubcommands(testBundle, "bundle.key", filter, extract) Expect(result).To(HaveLen(1)) Expect(result[0].key).To(Equal("nested.plugin/v1")) Expect(result[0].configKey).To(Equal("bundle.key")) }) }) Context("filterSubcommands", func() { var ( cli *CLI testPlugin1 *mockPluginWithSubcommand testPlugin2 *mockPluginWithSubcommand testSubcommand *mockTestSubcommand ) BeforeEach(func() { testSubcommand = &mockTestSubcommand{} testPlugin1 = newMockPluginWithSubcommand("plugin1", []config.Version{{Number: 1}}, testSubcommand) testPlugin2 = newMockPluginWithSubcommand("plugin2", []config.Version{{Number: 1}}, testSubcommand) cli = &CLI{ resolvedPlugins: []plugin.Plugin{testPlugin1, testPlugin2}, } }) It("should filter and extract subcommands from all plugins", func() { filter := func(p plugin.Plugin) bool { return p.Name() == "plugin1" } extract := func(p plugin.Plugin) plugin.Subcommand { if mp, ok := p.(*mockPluginWithSubcommand); ok { return mp.subcommand } return nil } result := cli.filterSubcommands(filter, extract) Expect(result).To(HaveLen(1)) Expect(result[0].key).To(Equal("plugin1/v1")) }) It("should return all subcommands when filter allows all", func() { filter := func(plugin.Plugin) bool { return true } extract := func(p plugin.Plugin) plugin.Subcommand { if mp, ok := p.(*mockPluginWithSubcommand); ok { return mp.subcommand } return nil } result := cli.filterSubcommands(filter, extract) Expect(result).To(HaveLen(2)) }) It("should return empty when filter rejects all", func() { filter := func(plugin.Plugin) bool { return false } extract := func(plugin.Plugin) plugin.Subcommand { return testSubcommand } result := cli.filterSubcommands(filter, extract) Expect(result).To(BeEmpty()) }) }) Context("duplicate flag handling (mergeFlagSetInto, syncDuplicateFlags)", func() { It("should not panic when merging two FlagSets that define the same flag name (same type)", func() { dest := pflag.NewFlagSet("dest", pflag.ExitOnError) src := pflag.NewFlagSet("src", pflag.ExitOnError) duplicateValues := make(map[string][]pflag.Value) firstPluginByFlag := make(map[string]string) var destBool bool var srcBool bool dest.BoolVar(&destBool, "force", false, "overwrite files (plugin A)") src.BoolVar(&srcBool, "force", false, "regenerate all files (plugin B)") err := mergeFlagSetInto(dest, src, duplicateValues, "pluginB/v1", firstPluginByFlag) Expect(err).NotTo(HaveOccurred()) Expect(dest.Lookup("force")).NotTo(BeNil()) Expect(duplicateValues["force"]).To(HaveLen(1)) }) It("should aggregate help text as For plugin (key): desc AND for plugin (key): desc", func() { dest := pflag.NewFlagSet("dest", pflag.ExitOnError) src := pflag.NewFlagSet("src", pflag.ExitOnError) duplicateValues := make(map[string][]pflag.Value) firstPluginByFlag := make(map[string]string) var a, b bool dest.BoolVar(&a, "force", false, "overwrite files (plugin A)") src.BoolVar(&b, "force", false, "regenerate all files (plugin B)") err := mergeFlagSetInto(dest, src, duplicateValues, "pluginB/v1", firstPluginByFlag) Expect(err).NotTo(HaveOccurred()) flag := dest.Lookup("force") Expect(flag).NotTo(BeNil()) Expect(flag.Usage).To(ContainSubstring("overwrite files (plugin A)")) Expect(flag.Usage).To(ContainSubstring("AND for plugin (pluginB/v1):")) Expect(flag.Usage).To(ContainSubstring("regenerate all files (plugin B)")) }) It("should prefix first plugin with For plugin (key): when both flags merged via mergeFlagSetInto", func() { dest := pflag.NewFlagSet("dest", pflag.ExitOnError) pluginA := pflag.NewFlagSet("a", pflag.ExitOnError) pluginB := pflag.NewFlagSet("b", pflag.ExitOnError) duplicateValues := make(map[string][]pflag.Value) firstPluginByFlag := make(map[string]string) var a, b bool pluginA.BoolVar(&a, "force", false, "overwrite files (plugin A)") pluginB.BoolVar(&b, "force", false, "regenerate all files (plugin B)") Expect(mergeFlagSetInto(dest, pluginA, duplicateValues, "pluginA/v1", firstPluginByFlag)).NotTo(HaveOccurred()) Expect(mergeFlagSetInto(dest, pluginB, duplicateValues, "pluginB/v1", firstPluginByFlag)).NotTo(HaveOccurred()) flag := dest.Lookup("force") Expect(flag).NotTo(BeNil()) Expect(flag.Usage).To(Equal( "For plugin (pluginA/v1): overwrite files (plugin A) AND for plugin (pluginB/v1): regenerate all files (plugin B)")) }) It("should show full plugin keys in aggregated usage", func() { dest := pflag.NewFlagSet("dest", pflag.ExitOnError) goPlugin := pflag.NewFlagSet("go", pflag.ExitOnError) helmPlugin := pflag.NewFlagSet("helm", pflag.ExitOnError) duplicateValues := make(map[string][]pflag.Value) firstPluginByFlag := make(map[string]string) var a, b bool goPlugin.BoolVar(&a, "force", false, "overwrite scaffolded files to apply changes (manual edits may be lost)") helmPlugin.BoolVar(&b, "force", false, "if true, regenerates all the files") Expect(mergeFlagSetInto(dest, goPlugin, duplicateValues, "base.go.kubebuilder.io/v4", firstPluginByFlag)). NotTo(HaveOccurred()) Expect(mergeFlagSetInto(dest, helmPlugin, duplicateValues, "helm.kubebuilder.io/v2-alpha", firstPluginByFlag)). NotTo(HaveOccurred()) flag := dest.Lookup("force") Expect(flag).NotTo(BeNil()) expectedUsage := "For plugin (base.go.kubebuilder.io/v4): overwrite scaffolded files to apply changes " + "(manual edits may be lost) AND for plugin (helm.kubebuilder.io/v2-alpha): if true, regenerates all the files" Expect(flag.Usage).To(Equal(expectedUsage)) }) It("should return error when same flag name is bound with different value types", func() { dest := pflag.NewFlagSet("dest", pflag.ExitOnError) src := pflag.NewFlagSet("src", pflag.ExitOnError) duplicateValues := make(map[string][]pflag.Value) firstPluginByFlag := make(map[string]string) firstPluginByFlag["flag"] = "pluginA/v1" // dest already has this flag from a previous plugin var a bool var b string dest.BoolVar(&a, "flag", false, "bool usage (plugin A)") src.StringVar(&b, "flag", "", "string usage (plugin B)") err := mergeFlagSetInto(dest, src, duplicateValues, "pluginB/v1", firstPluginByFlag) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("same flag name")) Expect(err.Error()).To(ContainSubstring("different value types")) Expect(err.Error()).To(ContainSubstring("flag")) Expect(err.Error()).To(ContainSubstring("bool")) Expect(err.Error()).To(ContainSubstring("string")) Expect(err.Error()).To(ContainSubstring("pluginA/v1")) Expect(err.Error()).To(ContainSubstring("pluginB/v1")) }) It("should sync parsed value to duplicate Values after syncDuplicateFlags", func() { flags := pflag.NewFlagSet("cmd", pflag.ExitOnError) var mainVal, dupVal bool flags.BoolVar(&mainVal, "force", false, "usage") tmpFS := pflag.NewFlagSet("", pflag.ExitOnError) tmpFS.BoolVar(&dupVal, "force", false, "") duplicateValues := map[string][]pflag.Value{ "force": {tmpFS.Lookup("force").Value}, } Expect(flags.Parse([]string{"--force", "true"})).NotTo(HaveOccurred()) Expect(mainVal).To(BeTrue()) Expect(dupVal).To(BeFalse()) syncDuplicateFlags(flags, duplicateValues) Expect(dupVal).To(BeTrue()) }) It("should give all plugins in the chain the same value for a shared flag (e.g. --force)", func() { cmdFlags := pflag.NewFlagSet("edit", pflag.ExitOnError) pluginA := pflag.NewFlagSet("pluginA", pflag.ExitOnError) pluginB := pflag.NewFlagSet("pluginB", pflag.ExitOnError) var forceA, forceB bool pluginA.BoolVar(&forceA, "force", false, "plugin A force") pluginB.BoolVar(&forceB, "force", false, "plugin B force") duplicateValues := make(map[string][]pflag.Value) firstPluginByFlag := make(map[string]string) Expect(mergeFlagSetInto(cmdFlags, pluginA, duplicateValues, "pluginA/v1", firstPluginByFlag)).NotTo(HaveOccurred()) Expect(mergeFlagSetInto(cmdFlags, pluginB, duplicateValues, "pluginB/v1", firstPluginByFlag)).NotTo(HaveOccurred()) Expect(cmdFlags.Parse([]string{"--force", "true"})).NotTo(HaveOccurred()) syncDuplicateFlags(cmdFlags, duplicateValues) Expect(forceA).To(BeTrue(), "plugin A must receive the value passed by the user") Expect(forceB).To(BeTrue(), "plugin B must receive the same value as the command") }) It("should sync string flag value to duplicate Values", func() { flags := pflag.NewFlagSet("cmd", pflag.ExitOnError) var mainVal, dupVal string flags.StringVar(&mainVal, "name", "", "name usage") tmpFS := pflag.NewFlagSet("", pflag.ExitOnError) tmpFS.StringVar(&dupVal, "name", "", "") duplicateValues := map[string][]pflag.Value{ "name": {tmpFS.Lookup("name").Value}, } Expect(flags.Parse([]string{"--name", "foo"})).NotTo(HaveOccurred()) syncDuplicateFlags(flags, duplicateValues) Expect(dupVal).To(Equal("foo")) }) It("applies merge and sync for any subcommand (init, api, webhook, edit), not only edit", func() { cmd := &cobra.Command{Use: "api"} pluginA := &mockSubcommandWithForceFlag{} pluginB := &mockSubcommandWithForceFlag{} tuples := []keySubcommandTuple{ {key: "pluginA.kubebuilder.io/v1", subcommand: pluginA}, {key: "pluginB.kubebuilder.io/v1", subcommand: pluginB}, } meta := plugin.CLIMetadata{} result, err := initializationHooks(cmd, tuples, meta) Expect(err).NotTo(HaveOccurred()) Expect(result.duplicateFlagValues["force"]).To(HaveLen(1), "second plugin's Value recorded as duplicate") Expect(cmd.ParseFlags([]string{"--force", "true"})).NotTo(HaveOccurred()) syncDuplicateFlags(cmd.Flags(), result.duplicateFlagValues) Expect(pluginA.Force).To(BeTrue(), "first plugin (flag on command) receives value") Expect(pluginB.Force).To(BeTrue(), "second plugin (duplicate) receives same value after sync") }) }) }) type mockTestSubcommand struct{} func (m *mockTestSubcommand) Scaffold(machinery.Filesystem) error { return nil } // mockSubcommandWithForceFlag implements Subcommand and HasFlags for tests with a shared flag. type mockSubcommandWithForceFlag struct { Force bool } func (m *mockSubcommandWithForceFlag) Scaffold(machinery.Filesystem) error { return nil } func (m *mockSubcommandWithForceFlag) BindFlags(flags *pflag.FlagSet) { flags.BoolVar(&m.Force, "force", false, "force usage") } type mockPluginWithSubcommand struct { name string supportedProjectVersions []config.Version subcommand plugin.Subcommand } func newMockPluginWithSubcommand( name string, versions []config.Version, subcommand plugin.Subcommand, ) *mockPluginWithSubcommand { return &mockPluginWithSubcommand{ name: name, supportedProjectVersions: versions, subcommand: subcommand, } } func (m *mockPluginWithSubcommand) Name() string { return m.name } func (m *mockPluginWithSubcommand) Version() plugin.Version { return plugin.Version{Number: 1} } func (m *mockPluginWithSubcommand) SupportedProjectVersions() []config.Version { return m.supportedProjectVersions } type mockPluginBundle struct { name string supportedProjectVersions []config.Version plugins []plugin.Plugin } func newMockPluginBundle(name string, versions []config.Version, plugins []plugin.Plugin) *mockPluginBundle { return &mockPluginBundle{ name: name, supportedProjectVersions: versions, plugins: plugins, } } func (m *mockPluginBundle) Name() string { return m.name } func (m *mockPluginBundle) Version() plugin.Version { return plugin.Version{Number: 1} } func (m *mockPluginBundle) SupportedProjectVersions() []config.Version { return m.supportedProjectVersions } func (m *mockPluginBundle) Plugins() []plugin.Plugin { return m.plugins } ================================================ FILE: pkg/cli/completion.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "fmt" "os" "github.com/spf13/cobra" ) func (c CLI) newBashCmd() *cobra.Command { return &cobra.Command{ Use: "bash", Short: "Load bash completions", Example: fmt.Sprintf(`# To load completion for this session, execute: $ source <(%[1]s completion bash) # To load completions for each session, execute once: Linux: $ %[1]s completion bash > /etc/bash_completion.d/%[1]s MacOS: $ %[1]s completion bash > /usr/local/etc/bash_completion.d/%[1]s `, c.commandName), RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Root().GenBashCompletionV2(os.Stdout, true) }, } } func (c CLI) newZshCmd() *cobra.Command { return &cobra.Command{ Use: "zsh", Short: "Load zsh completions", Example: fmt.Sprintf(`# If shell completion is not already enabled in your environment you will need # to enable it. You can execute the following once: $ echo "autoload -U compinit; compinit" >> ~/.zshrc # To load completions for each session, execute once: $ %[1]s completion zsh > "${fpath[1]}/_%[1]s" # You will need to start a new shell for this setup to take effect. `, c.commandName), RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Root().GenZshCompletion(os.Stdout) }, } } func (c CLI) newFishCmd() *cobra.Command { return &cobra.Command{ Use: "fish", Short: "Load fish completions", Example: fmt.Sprintf(`# To load completion for this session, execute: $ %[1]s completion fish | source # To load completions for each session, execute once: $ %[1]s completion fish > ~/.config/fish/completions/%[1]s.fish `, c.commandName), RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Root().GenFishCompletion(os.Stdout, true) }, } } func (CLI) newPowerShellCmd() *cobra.Command { return &cobra.Command{ Use: "powershell", Short: "Load powershell completions", RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Root().GenPowerShellCompletion(os.Stdout) }, } } func (c CLI) newCompletionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "completion", Short: "Load completions for the specified shell", Long: fmt.Sprintf(`Output shell completion code for the specified shell. The shell code must be evaluated to provide interactive completion of %[1]s commands. Detailed instructions on how to do this for each shell are provided in their own commands. `, c.commandName), } cmd.AddCommand(c.newBashCmd()) cmd.AddCommand(c.newZshCmd()) cmd.AddCommand(c.newFishCmd()) cmd.AddCommand(c.newPowerShellCmd()) return cmd } ================================================ FILE: pkg/cli/completion_test.go ================================================ /* Copyright 2022 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 cli import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Completion", func() { var c *CLI BeforeEach(func() { c = &CLI{} }) When("newBashCmd", func() { It("Testing the BashCompletion", func() { cmd := c.newBashCmd() Expect(cmd).NotTo(BeNil()) Expect(cmd.Use).NotTo(Equal("")) Expect(cmd.Use).To(ContainSubstring("bash")) Expect(cmd.Short).NotTo(Equal("")) Expect(cmd.Short).To(ContainSubstring("Load bash completions")) Expect(cmd.Example).NotTo(Equal("")) Expect(cmd.Example).To(ContainSubstring("# To load completion for this session")) }) }) Context("newZshCmd", func() { It("Testing the ZshCompletion", func() { cmd := c.newZshCmd() Expect(cmd).NotTo(BeNil()) Expect(cmd.Use).NotTo(Equal("")) Expect(cmd.Use).To(ContainSubstring("zsh")) Expect(cmd.Short).NotTo(Equal("")) Expect(cmd.Short).To(ContainSubstring("Load zsh completions")) Expect(cmd.Example).NotTo(Equal("")) Expect(cmd.Example).To(ContainSubstring("# If shell completion is not already enabled in your environment")) }) }) Context("newFishCmd", func() { It("Testing the FishCompletion", func() { cmd := c.newFishCmd() Expect(cmd).NotTo(BeNil()) Expect(cmd.Use).NotTo(Equal("")) Expect(cmd.Use).To(ContainSubstring("fish")) Expect(cmd.Short).NotTo(Equal("")) Expect(cmd.Short).To(ContainSubstring("Load fish completions")) Expect(cmd.Example).NotTo(Equal("")) Expect(cmd.Example).To(ContainSubstring("# To load completion for this session, execute:")) }) }) Context("newPowerShellCmd", func() { It("Testing the PowerShellCompletion", func() { cmd := c.newPowerShellCmd() Expect(cmd).NotTo(BeNil()) Expect(cmd.Use).NotTo(Equal("")) Expect(cmd.Use).To(ContainSubstring("powershell")) Expect(cmd.Short).NotTo(Equal("")) Expect(cmd.Short).To(ContainSubstring("Load powershell completions")) }) }) }) ================================================ FILE: pkg/cli/create.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "fmt" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) func (c CLI) newCreateCmd() *cobra.Command { return &cobra.Command{ Use: "create", SuggestFor: []string{"new"}, Short: "Scaffold a Kubernetes API or webhook", Long: fmt.Sprintf(`Scaffold a Kubernetes API or webhook. Available plugins that support 'create' subcommands: %s `, c.getPluginTableFilteredForSubcommand(func(p plugin.Plugin) bool { _, hasCreateAPI := p.(plugin.CreateAPI) _, hasCreateWebhook := p.(plugin.CreateWebhook) return hasCreateAPI || hasCreateWebhook })), } } ================================================ FILE: pkg/cli/doc.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ // Package cli provides the required tools to build a CLI utility that creates // scaffolds for operator projects. // // It is the entrypoint for any CLI that wants to use kubebuilder's scaffolding // capabilities. package cli ================================================ FILE: pkg/cli/edit.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ //nolint:dupl package cli import ( "fmt" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) const editErrorMsg = "failed to edit project" func (c CLI) newEditCmd() *cobra.Command { cmd := &cobra.Command{ Use: "edit", Short: "Update the project configuration", Long: `Edit the project configuration.`, RunE: errCmdFunc( fmt.Errorf("project must be initialized"), ), } // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { cmdErr(cmd, noResolvedPluginError{}) return cmd } // Obtain the plugin keys and subcommands from the plugins that implement plugin.Edit. subcommands := c.filterSubcommands( func(p plugin.Plugin) bool { _, isValid := p.(plugin.Edit) return isValid }, func(p plugin.Plugin) plugin.Subcommand { return p.(plugin.Edit).GetEditSubcommand() }, ) // Verify that there is at least one remaining plugin. if len(subcommands) == 0 { cmdErr(cmd, noAvailablePluginError{"edit project"}) return cmd } c.applySubcommandHooks(cmd, subcommands, editErrorMsg, false) // Append plugin table after metadata updates c.appendPluginTable(cmd, func(p plugin.Plugin) bool { _, isValid := p.(plugin.Edit) return isValid }, "Available plugins that support 'edit'") return cmd } ================================================ FILE: pkg/cli/init.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "fmt" "slices" "strconv" "strings" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) const initErrorMsg = "failed to initialize project" func (c CLI) newInitCmd() *cobra.Command { cmd := &cobra.Command{ Use: "init", Short: "Initialize a new project", Long: `Initialize a new project. For further help about a specific plugin, set --plugins. `, Example: c.getInitHelpExamples(), Run: func(_ *cobra.Command, _ []string) {}, } // Register --project-version on the dynamically created command // so that it shows up in help and does not cause a parse error. cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version") // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { cmdErr(cmd, noResolvedPluginError{}) return cmd } // Obtain the plugin keys and subcommands from the plugins that implement plugin.Init. subcommands := c.filterSubcommands( func(p plugin.Plugin) bool { _, isValid := p.(plugin.Init) return isValid }, func(p plugin.Plugin) plugin.Subcommand { return p.(plugin.Init).GetInitSubcommand() }, ) // Verify that there is at least one remaining plugin. if len(subcommands) == 0 { cmdErr(cmd, noAvailablePluginError{"project initialization"}) return cmd } c.applySubcommandHooks(cmd, subcommands, initErrorMsg, true) // Append plugin table after metadata updates c.appendPluginTable(cmd, func(p plugin.Plugin) bool { _, isValid := p.(plugin.Init) return isValid }, "Available plugins that support 'init'") return cmd } func (c CLI) getInitHelpExamples() string { var sb strings.Builder for _, version := range c.getAvailableProjectVersions() { rendered := fmt.Sprintf(` # Help for initializing a project with version %[2]s %[1]s init --project-version=%[2]s -h `, c.commandName, version) sb.WriteString(rendered) } return strings.TrimSuffix(sb.String(), "\n\n") } func (c CLI) getAvailableProjectVersions() (projectVersions []string) { versionSet := make(map[config.Version]struct{}) for _, p := range c.plugins { // Only return versions of non-deprecated plugins. if _, isDeprecated := p.(plugin.Deprecated); !isDeprecated { for _, version := range p.SupportedProjectVersions() { versionSet[version] = struct{}{} } } } for version := range versionSet { projectVersions = append(projectVersions, strconv.Quote(version.String())) } slices.Sort(projectVersions) return projectVersions } ================================================ FILE: pkg/cli/init_test.go ================================================ /* Copyright 2026 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 cli import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) var _ = Describe("init", func() { Context("getInitHelpExamples", func() { It("should return help examples for available project versions", func() { plugin1 := newMockPlugin("test.plugin", "3", config.Version{Number: 3}) plugin2 := newMockPlugin("another.plugin", "3", config.Version{Number: 3}, config.Version{Number: 4}) cli := CLI{ commandName: "kubebuilder", plugins: map[string]plugin.Plugin{ plugin.KeyFor(plugin1): plugin1, plugin.KeyFor(plugin2): plugin2, }, } examples := cli.getInitHelpExamples() Expect(examples).To(ContainSubstring("kubebuilder init")) Expect(examples).To(ContainSubstring("project-version")) Expect(examples).To(ContainSubstring("-h")) }) It("should handle multiple versions", func() { plugin1 := newMockPlugin("plugin1", "3", config.Version{Number: 2}) plugin2 := newMockPlugin("plugin2", "3", config.Version{Number: 3}) cli := CLI{ commandName: "kb", plugins: map[string]plugin.Plugin{ plugin.KeyFor(plugin1): plugin1, plugin.KeyFor(plugin2): plugin2, }, } examples := cli.getInitHelpExamples() Expect(examples).To(ContainSubstring("kb init")) Expect(examples).NotTo(HaveSuffix("\n\n")) }) It("should return empty string when no plugins", func() { cli := CLI{ commandName: "kubebuilder", plugins: map[string]plugin.Plugin{}, } examples := cli.getInitHelpExamples() Expect(examples).To(BeEmpty()) }) }) Context("getAvailableProjectVersions", func() { It("should return unique project versions", func() { plugin1 := newMockPlugin("plugin1", "3", config.Version{Number: 3}) plugin2 := newMockPlugin("plugin2", "3", config.Version{Number: 3}) plugin3 := newMockPlugin("plugin3", "3", config.Version{Number: 4}) cli := CLI{ plugins: map[string]plugin.Plugin{ plugin.KeyFor(plugin1): plugin1, plugin.KeyFor(plugin2): plugin2, plugin.KeyFor(plugin3): plugin3, }, } versions := cli.getAvailableProjectVersions() Expect(versions).To(ContainElement("\"3\"")) Expect(versions).To(ContainElement("\"4\"")) Expect(versions).To(HaveLen(2)) }) It("should exclude deprecated plugins", func() { deprecatedPlugin := newMockDeprecatedPlugin("deprecated", "3", "use v4 instead", config.Version{Number: 2}) regularPlugin := newMockPlugin("regular", "3", config.Version{Number: 3}) cli := CLI{ plugins: map[string]plugin.Plugin{ plugin.KeyFor(deprecatedPlugin): deprecatedPlugin, plugin.KeyFor(regularPlugin): regularPlugin, }, } versions := cli.getAvailableProjectVersions() Expect(versions).NotTo(ContainElement("\"2\"")) Expect(versions).To(ContainElement("\"3\"")) }) It("should return sorted versions", func() { plugin1 := newMockPlugin("plugin1", "3", config.Version{Number: 4}) plugin2 := newMockPlugin("plugin2", "3", config.Version{Number: 2}) plugin3 := newMockPlugin("plugin3", "3", config.Version{Number: 3}) cli := CLI{ plugins: map[string]plugin.Plugin{ plugin.KeyFor(plugin1): plugin1, plugin.KeyFor(plugin2): plugin2, plugin.KeyFor(plugin3): plugin3, }, } versions := cli.getAvailableProjectVersions() Expect(versions).To(Equal([]string{"\"2\"", "\"3\"", "\"4\""})) }) It("should return empty slice when no plugins", func() { cli := CLI{ plugins: map[string]plugin.Plugin{}, } versions := cli.getAvailableProjectVersions() Expect(versions).To(BeEmpty()) }) It("should handle plugin with multiple supported versions", func() { multiPlugin := newMockPlugin("multi", "3", config.Version{Number: 2}, config.Version{Number: 3}, config.Version{Number: 4}) cli := CLI{ plugins: map[string]plugin.Plugin{ plugin.KeyFor(multiPlugin): multiPlugin, }, } versions := cli.getAvailableProjectVersions() Expect(versions).To(HaveLen(3)) Expect(versions).To(ContainElement("\"2\"")) Expect(versions).To(ContainElement("\"3\"")) Expect(versions).To(ContainElement("\"4\"")) }) }) }) ================================================ FILE: pkg/cli/options.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "errors" "fmt" "io/fs" "log/slog" "os" "path/filepath" "runtime" "slices" "strings" "github.com/spf13/afero" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/external" ) var retrievePluginsRoot = getPluginsRoot // Option is a function used as arguments to New in order to configure the resulting CLI. type Option func(*CLI) error // WithCommandName is an Option that sets the CLI's root command name. func WithCommandName(name string) Option { return func(c *CLI) error { c.commandName = name return nil } } // WithVersion is an Option that defines the version string of the CLI. func WithVersion(version string) Option { return func(c *CLI) error { c.version = version return nil } } // WithCliVersion is an Option that defines only the version string of the CLI (no extra info). func WithCliVersion(version string) Option { return func(c *CLI) error { c.cliVersion = version return nil } } // WithDescription is an Option that sets the CLI's root description. func WithDescription(description string) Option { return func(c *CLI) error { c.description = description return nil } } // WithPlugins is an Option that sets the CLI's plugins. // // Specifying any invalid plugin results in an error. func WithPlugins(plugins ...plugin.Plugin) Option { return func(c *CLI) error { for _, p := range plugins { key := plugin.KeyFor(p) if _, isConflicting := c.plugins[key]; isConflicting { return fmt.Errorf("two plugins have the same key: %q", key) } if err := plugin.Validate(p); err != nil { return fmt.Errorf("broken pre-set plugin %q: %w", key, err) } c.plugins[key] = p } return nil } } // WithDefaultPlugins is an Option that sets the CLI's default plugins. // // Specifying any invalid plugin results in an error. func WithDefaultPlugins(projectVersion config.Version, plugins ...plugin.Plugin) Option { return func(c *CLI) error { if err := projectVersion.Validate(); err != nil { return fmt.Errorf("broken pre-set project version %q for default plugins: %w", projectVersion, err) } if len(plugins) == 0 { return fmt.Errorf("empty set of plugins provided for project version %q", projectVersion) } for _, p := range plugins { if err := plugin.Validate(p); err != nil { return fmt.Errorf("broken pre-set default plugin %q: %w", plugin.KeyFor(p), err) } if !plugin.SupportsVersion(p, projectVersion) { return fmt.Errorf("default plugin %q doesn't support version %q", plugin.KeyFor(p), projectVersion) } c.defaultPlugins[projectVersion] = append(c.defaultPlugins[projectVersion], plugin.KeyFor(p)) } return nil } } // WithDefaultProjectVersion is an Option that sets the CLI's default project version. // // Setting an invalid version results in an error. func WithDefaultProjectVersion(version config.Version) Option { return func(c *CLI) error { if err := version.Validate(); err != nil { return fmt.Errorf("broken pre-set default project version %q: %w", version, err) } c.defaultProjectVersion = version return nil } } // WithExtraCommands is an Option that adds extra subcommands to the CLI. // // Adding extra commands that duplicate existing commands results in an error. func WithExtraCommands(cmds ...*cobra.Command) Option { return func(c *CLI) error { // We don't know the commands defined by the CLI yet so we are not checking if the extra commands // conflict with a pre-existing one yet. We do this after creating the base commands. c.extraCommands = append(c.extraCommands, cmds...) return nil } } // WithExtraAlphaCommands is an Option that adds extra alpha subcommands to the CLI. // // Adding extra alpha commands that duplicate existing commands results in an error. func WithExtraAlphaCommands(cmds ...*cobra.Command) Option { return func(c *CLI) error { // We don't know the commands defined by the CLI yet so we are not checking if the extra alpha commands // conflict with a pre-existing one yet. We do this after creating the base commands. c.extraAlphaCommands = append(c.extraAlphaCommands, cmds...) return nil } } // WithCompletion is an Option that adds the completion subcommand. func WithCompletion() Option { return func(c *CLI) error { c.completionCommand = true return nil } } // WithFilesystem is an Option that allows to set the filesystem used in the CLI. func WithFilesystem(filesystem machinery.Filesystem) Option { return func(c *CLI) error { if filesystem.FS == nil { return errors.New("invalid filesystem") } c.fs = filesystem return nil } } // parseExternalPluginArgs returns the program arguments. func parseExternalPluginArgs() (args []string) { // Loop through os.Args and only get flags and their values that should be passed to the plugins // this also removes the --plugins flag and its values from the list passed to the external plugin for i := range os.Args { if strings.Contains(os.Args[i], "--") && !strings.Contains(os.Args[i], "--plugins") { args = append(args, os.Args[i]) // Don't go out of bounds and don't append the next value if it is a flag if i+1 < len(os.Args) && !strings.Contains(os.Args[i+1], "--") { args = append(args, os.Args[i+1]) } } } return args } // isHostSupported checks whether the host system is supported or not. func isHostSupported(host string) bool { return slices.Contains(supportedPlatforms, host) } // getPluginsRoot gets the plugin root path. func getPluginsRoot(host string) (pluginsRoot string, err error) { if !isHostSupported(host) { // freebsd, openbsd, windows... return "", fmt.Errorf("host not supported: %v", host) } // if user provides specific path, return if pluginsPath := os.Getenv("EXTERNAL_PLUGINS_PATH"); pluginsPath != "" { // verify if the path actually exists if _, err = os.Stat(pluginsPath); err != nil { if os.IsNotExist(err) { // the path does not exist return "", fmt.Errorf("the specified path %s does not exist", pluginsPath) } // some other error return "", fmt.Errorf("error checking the path: %w", err) } // the path exists return pluginsPath, nil } // if no specific path, detects the host system and gets the plugins root based on the host. pluginsRelativePath := filepath.Join("kubebuilder", "plugins") if xdgHome := os.Getenv("XDG_CONFIG_HOME"); xdgHome != "" { return filepath.Join(xdgHome, pluginsRelativePath), nil } switch host { case "darwin": slog.Debug("Detected host is macOS.") pluginsRoot = filepath.Join("Library", "Application Support", pluginsRelativePath) case "linux": slog.Debug("Detected host is Linux.") pluginsRoot = filepath.Join(".config", pluginsRelativePath) } userHomeDir, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("error retrieving home dir: %w", err) } return filepath.Join(userHomeDir, pluginsRoot), nil } // DiscoverExternalPlugins discovers the external plugins in the plugins root directory // and adds them to external.Plugin. func DiscoverExternalPlugins(filesystem afero.Fs) (ps []plugin.Plugin, err error) { pluginsRoot, err := retrievePluginsRoot(runtime.GOOS) if err != nil { slog.Error("could not get plugins root", "error", err) return nil, fmt.Errorf("could not get plugins root: %w", err) } rootInfo, err := filesystem.Stat(pluginsRoot) if err != nil { if errors.Is(err, afero.ErrFileNotFound) { slog.Debug("External plugins dir does not exist, skipping external plugin parsing", "dir", pluginsRoot) return nil, nil } return nil, fmt.Errorf("error getting stats for plugins %s: %w", pluginsRoot, err) } if !rootInfo.IsDir() { slog.Debug("External plugins path is not a directory, skipping external plugin parsing", "path", pluginsRoot) return nil, nil } pluginInfos, err := afero.ReadDir(filesystem, pluginsRoot) if err != nil { return nil, fmt.Errorf("error reading plugins directory %q: %w", pluginsRoot, err) } for _, pluginInfo := range pluginInfos { if !pluginInfo.IsDir() { slog.Debug("skipping parsing, not a directory", "name", pluginInfo.Name()) continue } versions, err := afero.ReadDir(filesystem, filepath.Join(pluginsRoot, pluginInfo.Name())) if err != nil { return nil, fmt.Errorf("error reading plugin directory %s: %w", filepath.Join(pluginsRoot, pluginInfo.Name()), err) } for _, version := range versions { if !version.IsDir() { slog.Debug("skipping parsing, not a directory", "name", version.Name()) continue } pluginFiles, err := afero.ReadDir(filesystem, filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name())) if err != nil { return nil, fmt.Errorf("error reading plugion version directory %q: %w", filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name()), err) } for _, pluginFile := range pluginFiles { // find the executable that matches the same name as info.Name(). // if no match is found, compare the external plugin string name before dot // and match it with info.Name() which is the external plugin root dir. // for example: sample.sh --> sample, externalplugin.py --> externalplugin trimmedPluginName := strings.Split(pluginFile.Name(), ".") if trimmedPluginName[0] == "" { return nil, fmt.Errorf("invalid plugin name found %q", pluginFile.Name()) } if pluginFile.Name() == pluginInfo.Name() || trimmedPluginName[0] == pluginInfo.Name() { // check whether the external plugin is an executable. if !isPluginExecutable(pluginFile.Mode()) { return nil, fmt.Errorf("external plugin %q found in path is not an executable", pluginFile.Name()) } ep := external.Plugin{ PName: pluginInfo.Name(), Path: filepath.Join(pluginsRoot, pluginInfo.Name(), version.Name(), pluginFile.Name()), PSupportedProjectVersions: []config.Version{cfgv3.Version}, Args: parseExternalPluginArgs(), } if err = ep.PVersion.Parse(version.Name()); err != nil { return nil, fmt.Errorf("error parsing external plugin version %q: %w", version.Name(), err) } slog.Info("Adding external plugin", "plugin name", ep.Name()) ps = append(ps, ep) } } } } return ps, nil } // isPluginExecutable checks if a plugin is an executable based on the bitmask and returns true or false. func isPluginExecutable(mode fs.FileMode) bool { return mode&0o111 != 0 } ================================================ FILE: pkg/cli/options_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "errors" "fmt" "io/fs" "os" "path/filepath" "runtime" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/afero" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) var _ = Describe("Discover external plugins", func() { Context("with valid plugins root path", func() { var ( homePath string customPath string // store user's original EXTERNAL_PLUGINS_PATH originalPluginPath string xdghome string // store user's original XDG_CONFIG_HOME originalXdghome string ) BeforeEach(func() { homePath = os.Getenv("HOME") customPath = "/tmp/myplugins" }) When("XDG_CONFIG_HOME is not set and using the $HOME environment variable", func() { // store and unset the XDG_CONFIG_HOME BeforeEach(func() { originalXdghome = os.Getenv("XDG_CONFIG_HOME") err := os.Unsetenv("XDG_CONFIG_HOME") Expect(err).ToNot(HaveOccurred()) }) AfterEach(func() { if originalXdghome != "" { // restore the original value err := os.Setenv("XDG_CONFIG_HOME", originalXdghome) Expect(err).ToNot(HaveOccurred()) } }) It("should return the correct path for the darwin OS", func() { plgPath, err := getPluginsRoot("darwin") Expect(err).ToNot(HaveOccurred()) Expect(plgPath).To(Equal(fmt.Sprintf("%s/Library/Application Support/kubebuilder/plugins", homePath))) }) It("should return the correct path for the linux OS", func() { plgPath, err := getPluginsRoot("linux") Expect(err).ToNot(HaveOccurred()) Expect(plgPath).To(Equal(fmt.Sprintf("%s/.config/kubebuilder/plugins", homePath))) }) It("should return error when the host is not darwin / linux", func() { plgPath, err := getPluginsRoot("random") Expect(plgPath).To(Equal("")) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("host not supported")) }) }) When("XDG_CONFIG_HOME is set", func() { BeforeEach(func() { // store and set the XDG_CONFIG_HOME originalXdghome = os.Getenv("XDG_CONFIG_HOME") err := os.Setenv("XDG_CONFIG_HOME", fmt.Sprintf("%s/.config", homePath)) Expect(err).ToNot(HaveOccurred()) xdghome = os.Getenv("XDG_CONFIG_HOME") }) AfterEach(func() { if originalXdghome != "" { // restore the original value err := os.Setenv("XDG_CONFIG_HOME", originalXdghome) Expect(err).ToNot(HaveOccurred()) } else { // unset if it was originally unset err := os.Unsetenv("XDG_CONFIG_HOME") Expect(err).ToNot(HaveOccurred()) } }) It("should return the correct path for the darwin OS", func() { plgPath, err := getPluginsRoot("darwin") Expect(err).ToNot(HaveOccurred()) Expect(plgPath).To(Equal(fmt.Sprintf("%s/kubebuilder/plugins", xdghome))) }) It("should return the correct path for the linux OS", func() { plgPath, err := getPluginsRoot("linux") Expect(err).ToNot(HaveOccurred()) Expect(plgPath).To(Equal(fmt.Sprintf("%s/kubebuilder/plugins", xdghome))) }) It("should return error when the host is not darwin / linux", func() { plgPath, err := getPluginsRoot("random") Expect(plgPath).To(Equal("")) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("host not supported")) }) }) When("using the custom path", func() { BeforeEach(func() { err := os.MkdirAll(customPath, 0o750) Expect(err).ToNot(HaveOccurred()) // store and set the EXTERNAL_PLUGINS_PATH originalPluginPath = os.Getenv("EXTERNAL_PLUGINS_PATH") err = os.Setenv("EXTERNAL_PLUGINS_PATH", customPath) Expect(err).ToNot(HaveOccurred()) }) AfterEach(func() { if originalPluginPath != "" { // restore the original value err := os.Setenv("EXTERNAL_PLUGINS_PATH", originalPluginPath) Expect(err).ToNot(HaveOccurred()) } else { // unset if it was originally unset err := os.Unsetenv("EXTERNAL_PLUGINS_PATH") Expect(err).ToNot(HaveOccurred()) } }) It("should return the user given path for darwin OS", func() { plgPath, err := getPluginsRoot("darwin") Expect(plgPath).To(Equal(customPath)) Expect(err).ToNot(HaveOccurred()) }) It("should return the user given path for linux OS", func() { plgPath, err := getPluginsRoot("linux") Expect(plgPath).To(Equal(customPath)) Expect(err).ToNot(HaveOccurred()) }) It("should report error when the host is not darwin / linux", func() { plgPath, err := getPluginsRoot("random") Expect(plgPath).To(Equal("")) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("host not supported")) }) }) }) Context("with invalid plugins root path", func() { var originalPluginPath string BeforeEach(func() { originalPluginPath = os.Getenv("EXTERNAL_PLUGINS_PATH") err := os.Setenv("EXTERNAL_PLUGINS_PATH", "/non/existent/path") Expect(err).ToNot(HaveOccurred()) }) AfterEach(func() { if originalPluginPath != "" { // restore the original value err := os.Setenv("EXTERNAL_PLUGINS_PATH", originalPluginPath) Expect(err).ToNot(HaveOccurred()) } else { // unset if it was originally unset err := os.Unsetenv("EXTERNAL_PLUGINS_PATH") Expect(err).ToNot(HaveOccurred()) } }) It("should return an error for the darwin OS", func() { plgPath, err := getPluginsRoot("darwin") Expect(err).To(HaveOccurred()) Expect(plgPath).To(Equal("")) }) It("should return an error for the linux OS", func() { plgPath, err := getPluginsRoot("linux") Expect(err).To(HaveOccurred()) Expect(plgPath).To(Equal("")) }) It("should return an error when the host is not darwin / linux", func() { plgPath, err := getPluginsRoot("random") Expect(err).To(HaveOccurred()) Expect(plgPath).To(Equal("")) }) }) Context("when plugin executables exist in the expected plugin directories", func() { const ( filePermissions os.FileMode = 755 testPluginScript = `#!/bin/bash echo "This is an external plugin" ` ) var ( pluginFilePath string pluginFileName string pluginPath string pluginsRoot string plugins []plugin.Plugin f afero.File filesystem machinery.Filesystem err error ) BeforeEach(func() { filesystem = machinery.Filesystem{ FS: afero.NewMemMapFs(), } pluginPath, err = getPluginsRoot(runtime.GOOS) Expect(err).ToNot(HaveOccurred()) pluginFileName = "externalPlugin.sh" pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName) err = filesystem.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700) Expect(err).ToNot(HaveOccurred()) f, err = filesystem.FS.Create(pluginFilePath) Expect(err).ToNot(HaveOccurred()) Expect(f).ToNot(BeNil()) _, err = filesystem.FS.Stat(pluginFilePath) Expect(err).ToNot(HaveOccurred()) }) It("should discover the external plugin executable without any errors", func() { // test that DiscoverExternalPlugins works if the plugin file is an executable and // is found in the expected path _, err = f.WriteString(testPluginScript) Expect(err).To(Not(HaveOccurred())) err = filesystem.FS.Chmod(pluginFilePath, filePermissions) Expect(err).To(Not(HaveOccurred())) _, err = filesystem.FS.Stat(pluginFilePath) Expect(err).ToNot(HaveOccurred()) plugins, err = DiscoverExternalPlugins(filesystem.FS) Expect(err).ToNot(HaveOccurred()) Expect(plugins).NotTo(BeNil()) Expect(plugins).To(HaveLen(1)) Expect(plugins[0].Name()).To(Equal("externalPlugin")) Expect(plugins[0].Version().Number).To(Equal(1)) }) It("should discover multiple external plugins and return the plugins without any errors", func() { // set the execute permissions on the first plugin executable err = filesystem.FS.Chmod(pluginFilePath, filePermissions) pluginFileName = "myotherexternalPlugin.sh" pluginFilePath = filepath.Join(pluginPath, "myotherexternalPlugin", "v1", pluginFileName) f, err = filesystem.FS.Create(pluginFilePath) Expect(err).ToNot(HaveOccurred()) Expect(f).ToNot(BeNil()) _, err = filesystem.FS.Stat(pluginFilePath) Expect(err).ToNot(HaveOccurred()) _, err = f.WriteString(testPluginScript) Expect(err).To(Not(HaveOccurred())) // set the execute permissions on the second plugin executable err = filesystem.FS.Chmod(pluginFilePath, filePermissions) Expect(err).To(Not(HaveOccurred())) _, err = filesystem.FS.Stat(pluginFilePath) Expect(err).ToNot(HaveOccurred()) plugins, err = DiscoverExternalPlugins(filesystem.FS) Expect(err).ToNot(HaveOccurred()) Expect(plugins).NotTo(BeNil()) Expect(plugins).To(HaveLen(2)) Expect(plugins[0].Name()).To(Equal("externalPlugin")) Expect(plugins[1].Name()).To(Equal("myotherexternalPlugin")) }) Context("that are invalid", func() { BeforeEach(func() { filesystem = machinery.Filesystem{ FS: afero.NewMemMapFs(), } pluginPath, err = getPluginsRoot(runtime.GOOS) Expect(err).ToNot(HaveOccurred()) }) It("should error if the plugin found is not an executable", func() { pluginFileName = "externalPlugin.sh" pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName) err = filesystem.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700) Expect(err).ToNot(HaveOccurred()) var file fs.File file, err = filesystem.FS.Create(pluginFilePath) Expect(err).ToNot(HaveOccurred()) Expect(file).ToNot(BeNil()) _, err = filesystem.FS.Stat(pluginFilePath) Expect(err).ToNot(HaveOccurred()) // set the plugin file permissions to read-only err = filesystem.FS.Chmod(pluginFilePath, 0o444) Expect(err).To(Not(HaveOccurred())) plugins, err = DiscoverExternalPlugins(filesystem.FS) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("not an executable")) Expect(plugins).To(BeEmpty()) }) It("should error if the plugin found has an invalid plugin name", func() { pluginFileName = ".sh" pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName) err = filesystem.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700) Expect(err).ToNot(HaveOccurred()) f, err = filesystem.FS.Create(pluginFilePath) Expect(err).ToNot(HaveOccurred()) Expect(f).ToNot(BeNil()) plugins, err = DiscoverExternalPlugins(filesystem.FS) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("invalid plugin name found")) Expect(plugins).To(BeEmpty()) }) }) Context("that does not match the plugin root directory name", func() { BeforeEach(func() { filesystem = machinery.Filesystem{ FS: afero.NewMemMapFs(), } pluginPath, err = getPluginsRoot(runtime.GOOS) Expect(err).ToNot(HaveOccurred()) }) It("should skip adding the external plugin and not return any errors", func() { pluginFileName = "random.sh" pluginFilePath = filepath.Join(pluginPath, "externalPlugin", "v1", pluginFileName) err = filesystem.FS.MkdirAll(filepath.Dir(pluginFilePath), 0o700) Expect(err).ToNot(HaveOccurred()) f, err = filesystem.FS.Create(pluginFilePath) Expect(err).ToNot(HaveOccurred()) Expect(f).ToNot(BeNil()) err = filesystem.FS.Chmod(pluginFilePath, filePermissions) Expect(err).ToNot(HaveOccurred()) var ps []plugin.Plugin ps, err = DiscoverExternalPlugins(filesystem.FS) Expect(err).ToNot(HaveOccurred()) Expect(ps).To(BeEmpty()) }) It("should fail if pluginsroot is empty", func() { errPluginsRoot := errors.New("could not retrieve plugins root") retrievePluginsRoot = func(_ string) (string, error) { return "", errPluginsRoot } _, err = DiscoverExternalPlugins(filesystem.FS) Expect(err).To(HaveOccurred()) Expect(errors.Unwrap(err)).To(MatchError(errPluginsRoot)) }) It("should skip parsing of directories if plugins root is not a directory", func() { retrievePluginsRoot = func(_ string) (string, error) { return "externalplugin.sh", nil } _, err = DiscoverExternalPlugins(filesystem.FS) Expect(err).ToNot(HaveOccurred()) }) It("should return full path to the external plugins without XDG_CONFIG_HOME", func() { if _, ok := os.LookupEnv("XDG_CONFIG_HOME"); ok { err = os.Setenv("XDG_CONFIG_HOME", "") Expect(err).ToNot(HaveOccurred()) } home := os.Getenv("HOME") pluginsRoot, err = getPluginsRoot("darwin") Expect(err).ToNot(HaveOccurred()) expected := filepath.Join(home, "Library", "Application Support", "kubebuilder", "plugins") Expect(pluginsRoot).To(Equal(expected)) pluginsRoot, err = getPluginsRoot("linux") Expect(err).ToNot(HaveOccurred()) expected = filepath.Join(home, ".config", "kubebuilder", "plugins") Expect(pluginsRoot).To(Equal(expected)) }) It("should return full path to the external plugins with XDG_CONFIG_HOME", func() { err = os.Setenv("XDG_CONFIG_HOME", "/some/random/path") Expect(err).ToNot(HaveOccurred()) pluginsRoot, err = getPluginsRoot(runtime.GOOS) Expect(err).ToNot(HaveOccurred()) Expect(pluginsRoot).To(Equal("/some/random/path/kubebuilder/plugins")) }) It("should return error when home directory is set to empty", func() { _, ok := os.LookupEnv("XDG_CONFIG_HOME") if ok { err = os.Setenv("XDG_CONFIG_HOME", "") Expect(err).ToNot(HaveOccurred()) } _, ok = os.LookupEnv("HOME") if ok { err = os.Setenv("HOME", "") Expect(err).ToNot(HaveOccurred()) } pluginsRoot, err = getPluginsRoot(runtime.GOOS) Expect(err).To(HaveOccurred()) Expect(pluginsRoot).To(Equal("")) Expect(err.Error()).To(ContainSubstring("error retrieving home dir")) }) }) }) Context("parsing flags for external plugins", func() { It("should only parse flags excluding the `--plugins` flag", func() { // change the os.Args for this test and set them back after oldArgs := os.Args defer func() { os.Args = oldArgs }() os.Args = []string{ "kubebuilder", "init", "--plugins", "myexternalplugin/v1", "--domain", "example.com", "--binary-flag", "--license", "apache2", "--another-binary", } args := parseExternalPluginArgs() Expect(args).Should(ContainElements( "--domain", "example.com", "--binary-flag", "--license", "apache2", "--another-binary", )) Expect(args).ShouldNot(ContainElements( "kubebuilder", "init", "--plugins", "myexternalplugin/v1", )) }) }) }) var _ = Describe("CLI options", func() { const ( pluginName = "plugin" pluginVersion = "v1" ) var ( c *CLI err error projectVersion config.Version p plugin.Plugin np1 plugin.Plugin np2 mockPlugin np3 plugin.Plugin np4 plugin.Plugin ) BeforeEach(func() { projectVersion = config.Version{Number: 1} p = newMockPlugin(pluginName, pluginVersion, projectVersion) np1 = newMockPlugin("Plugin", pluginVersion, projectVersion) np2 = mockPlugin{pluginName, plugin.Version{Number: -1}, []config.Version{projectVersion}} np3 = newMockPlugin(pluginName, pluginVersion) np4 = newMockPlugin(pluginName, pluginVersion, config.Version{}) }) Context("WithCommandName", func() { It("should use provided command name", func() { commandName := "other-command" c, err = newCLI(WithCommandName(commandName)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.commandName).To(Equal(commandName)) }) }) Context("WithVersion", func() { It("should use the provided version string", func() { version := "Version: 0.0.0" c, err = newCLI(WithVersion(version)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.version).To(Equal(version)) }) }) Context("WithCliVersion", func() { It("should use the provided CLI version string", func() { cliVersion := "v4.0.0" c, err = newCLI(WithCliVersion(cliVersion)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.cliVersion).To(Equal(cliVersion)) }) }) Context("WithDescription", func() { It("should use the provided description string", func() { description := "alternative description" c, err = newCLI(WithDescription(description)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.description).To(Equal(description)) }) }) Context("WithPlugins", func() { It("should return a valid CLI", func() { c, err = newCLI(WithPlugins(p)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.plugins).To(Equal(map[string]plugin.Plugin{plugin.KeyFor(p): p})) }) When("providing plugins with same keys", func() { It("should return an error", func() { _, err = newCLI(WithPlugins(p, p)) Expect(err).To(HaveOccurred()) }) }) When("providing plugins with same keys in two steps", func() { It("should return an error", func() { _, err = newCLI(WithPlugins(p), WithPlugins(p)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid name", func() { It("should return an error", func() { _, err = newCLI(WithPlugins(np1)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid version", func() { It("should return an error", func() { _, err = newCLI(WithPlugins(np2)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an empty list of supported versions", func() { It("should return an error", func() { _, err = newCLI(WithPlugins(np3)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid list of supported versions", func() { It("should return an error", func() { _, err = newCLI(WithPlugins(np4)) Expect(err).To(HaveOccurred()) }) }) }) Context("WithDefaultPlugins", func() { It("should return a valid CLI", func() { c, err = newCLI(WithDefaultPlugins(projectVersion, p)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.defaultPlugins).To(Equal(map[config.Version][]string{projectVersion: {plugin.KeyFor(p)}})) }) When("providing an invalid project version", func() { It("should return an error", func() { _, err = newCLI(WithDefaultPlugins(config.Version{}, p)) Expect(err).To(HaveOccurred()) }) }) When("providing an empty set of plugins", func() { It("should return an error", func() { _, err = newCLI(WithDefaultPlugins(projectVersion)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid name", func() { It("should return an error", func() { _, err = newCLI(WithDefaultPlugins(projectVersion, np1)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid version", func() { It("should return an error", func() { _, err = newCLI(WithDefaultPlugins(projectVersion, np2)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an empty list of supported versions", func() { It("should return an error", func() { _, err = newCLI(WithDefaultPlugins(projectVersion, np3)) Expect(err).To(HaveOccurred()) }) }) When("providing a plugin with an invalid list of supported versions", func() { It("should return an error", func() { _, err = newCLI(WithDefaultPlugins(projectVersion, np4)) Expect(err).To(HaveOccurred()) }) }) When("providing a default plugin for an unsupported project version", func() { It("should return an error", func() { _, err = newCLI(WithDefaultPlugins(config.Version{Number: 2}, p)) Expect(err).To(HaveOccurred()) }) }) }) Context("WithDefaultProjectVersion", func() { DescribeTable("should return a valid CLI", func(projectVersion config.Version) { c, err = newCLI(WithDefaultProjectVersion(projectVersion)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.defaultProjectVersion).To(Equal(projectVersion)) }, Entry("for version `2`", config.Version{Number: 2}), Entry("for version `3-alpha`", config.Version{Number: 3, Stage: stage.Alpha}), Entry("for version `3`", config.Version{Number: 3}), ) DescribeTable("should fail", func(projectVersion config.Version) { _, err = newCLI(WithDefaultProjectVersion(projectVersion)) Expect(err).To(HaveOccurred()) }, Entry("for empty version", config.Version{}), Entry("for invalid stage", config.Version{Number: 1, Stage: stage.Stage(27)}), ) }) Context("WithExtraCommands", func() { It("should return a valid CLI with extra commands", func() { commandTest := &cobra.Command{ Use: "example", } c, err = newCLI(WithExtraCommands(commandTest)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.extraCommands).NotTo(BeNil()) Expect(c.extraCommands).To(HaveLen(1)) Expect(c.extraCommands[0]).NotTo(BeNil()) Expect(c.extraCommands[0].Use).To(Equal(commandTest.Use)) }) }) Context("WithExtraAlphaCommands", func() { It("should return a valid CLI with extra alpha commands", func() { commandTest := &cobra.Command{ Use: "example", } c, err = newCLI(WithExtraAlphaCommands(commandTest)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.extraAlphaCommands).NotTo(BeNil()) Expect(c.extraAlphaCommands).To(HaveLen(1)) Expect(c.extraAlphaCommands[0]).NotTo(BeNil()) Expect(c.extraAlphaCommands[0].Use).To(Equal(commandTest.Use)) }) }) Context("WithCompletion", func() { It("should not add the completion command by default", func() { c, err = newCLI() Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.completionCommand).To(BeFalse()) }) It("should add the completion command if requested", func() { c, err = newCLI(WithCompletion()) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.completionCommand).To(BeTrue()) }) }) Context("WithFilesystem", func() { When("providing a valid filesystem", func() { It("should use the provided filesystem", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } c, err = newCLI(WithFilesystem(fs)) Expect(err).NotTo(HaveOccurred()) Expect(c).NotTo(BeNil()) Expect(c.fs).To(Equal(fs)) }) }) When("providing a invalid filesystem", func() { It("should return an error", func() { fs := machinery.Filesystem{} c, err = newCLI(WithFilesystem(fs)) Expect(err).To(HaveOccurred()) Expect(c).To(BeNil()) }) }) }) }) ================================================ FILE: pkg/cli/resource.go ================================================ /* Copyright 2022 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 cli import ( "errors" "strings" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) const ( groupPresent = "group flag present but empty" versionPresent = "version flag present but empty" kindPresent = "kind flag present but empty" ) // resourceOptions contains the information required to build a new resource.Resource. type resourceOptions struct { resource.GVK } func bindResourceFlags(fs *pflag.FlagSet) *resourceOptions { options := &resourceOptions{} fs.StringVar(&options.Group, "group", "", "resource Group") fs.StringVar(&options.Version, "version", "", "resource Version") fs.StringVar(&options.Kind, "kind", "", "resource Kind") return options } // validate verifies that all the fields have valid values. func (opts resourceOptions) validate() error { // Check that the required flags did not get a flag as their value. // We can safely look for a '-' as the first char as none of the fields accepts it. // NOTE: We must do this for all the required flags first or we may output the wrong // error as flags may seem to be missing because Cobra assigned them to another flag. if strings.HasPrefix(opts.Group, "-") { return errors.New(groupPresent) } if strings.HasPrefix(opts.Version, "-") { return errors.New(versionPresent) } if strings.HasPrefix(opts.Kind, "-") { return errors.New(kindPresent) } // We do not check here if the GVK values are empty because that would // make them mandatory and some plugins may want to set default values. // Instead, this is checked by resource.GVK.Validate() return nil } // newResource creates a new resource from the options func (opts resourceOptions) newResource() *resource.Resource { return &resource.Resource{ GVK: resource.GVK{ // Remove whitespaces to prevent values like " " pass validation Group: strings.TrimSpace(opts.Group), Domain: strings.TrimSpace(opts.Domain), Version: strings.TrimSpace(opts.Version), Kind: strings.TrimSpace(opts.Kind), }, Plural: resource.RegularPlural(opts.Kind), API: &resource.API{}, Webhooks: &resource.Webhooks{}, } } ================================================ FILE: pkg/cli/resource_test.go ================================================ /* Copyright 2022 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 cli import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) var _ = Describe("resourceOptions", func() { const ( group = "crew" domain = "test.io" version = "v1" kind = "FirstMate" ) var ( fullGVK resource.GVK noDomainGVK resource.GVK noGroupGVK resource.GVK ) BeforeEach(func() { fullGVK = resource.GVK{ Group: group, Domain: domain, Version: version, Kind: kind, } noDomainGVK = resource.GVK{ Group: group, Version: version, Kind: kind, } noGroupGVK = resource.GVK{ Domain: domain, Version: version, Kind: kind, } }) Context("validate", func() { DescribeTable("should succeed for valid options", func(options resourceOptions) { Expect(options.validate()).To(Succeed()) }, Entry("full GVK", resourceOptions{GVK: fullGVK}), Entry("missing domain", resourceOptions{GVK: noDomainGVK}), Entry("missing group", resourceOptions{GVK: noGroupGVK}), ) DescribeTable("should fail for invalid options", func(options resourceOptions) { Expect(options.validate()).NotTo(Succeed()) }, Entry("group flag captured another flag", resourceOptions{GVK: resource.GVK{Group: "--version"}}), Entry("version flag captured another flag", resourceOptions{GVK: resource.GVK{Version: "--kind"}}), Entry("kind flag captured another flag", resourceOptions{GVK: resource.GVK{Kind: "--group"}}), ) }) Context("newResource", func() { DescribeTable("should succeed if the Resource is valid", func(getOpts func() resourceOptions) { options := getOpts() Expect(options.validate()).To(Succeed()) resource := options.newResource() Expect(resource.Validate()).To(Succeed()) Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) Expect(resource.Path).To(Equal("")) Expect(resource.API).NotTo(BeNil()) Expect(resource.API.CRDVersion).To(Equal("")) Expect(resource.API.Namespaced).To(BeFalse()) Expect(resource.Controller).To(BeFalse()) Expect(resource.Webhooks).NotTo(BeNil()) Expect(resource.Webhooks.WebhookVersion).To(Equal("")) Expect(resource.Webhooks.Defaulting).To(BeFalse()) Expect(resource.Webhooks.Validation).To(BeFalse()) Expect(resource.Webhooks.Conversion).To(BeFalse()) }, Entry("full GVK", func() resourceOptions { return resourceOptions{GVK: fullGVK} }), Entry("missing domain", func() resourceOptions { return resourceOptions{GVK: noDomainGVK} }), Entry("missing group", func() resourceOptions { return resourceOptions{GVK: noGroupGVK} }), ) DescribeTable("should default the Plural by pluralizing the Kind", func(kind, plural string) { options := resourceOptions{GVK: resource.GVK{Group: group, Version: version, Kind: kind}} Expect(options.validate()).To(Succeed()) resource := options.newResource() Expect(resource.Validate()).To(Succeed()) Expect(resource.GVK.IsEqualTo(options.GVK)).To(BeTrue()) Expect(resource.Plural).To(Equal(plural)) }, Entry("for `FirstMate`", "FirstMate", "firstmates"), Entry("for `Fish`", "Fish", "fish"), Entry("for `Helmswoman`", "Helmswoman", "helmswomen"), ) }) }) ================================================ FILE: pkg/cli/root.go ================================================ /* Copyright 2022 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 cli import ( "errors" "fmt" "slices" "strings" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) var ( supportedPlatforms = []string{"darwin", "linux"} // errHelpDisplayed is returned when help is displayed to prevent command execution errHelpDisplayed = errors.New("help displayed") ) // isHelpFlag checks if the given string is a help flag func isHelpFlag(s string) bool { return s == "--help" || s == "-h" || s == "help" } // getShortKey converts a full plugin key to a short display key // Example: "deploy-image.go.kubebuilder.io/v1-alpha" -> "deploy-image/v1-alpha" func getShortKey(fullKey string) string { name, version := plugin.SplitKey(fullKey) // Extract the short name (part before .kubebuilder.io or other domain) shortName := name if strings.Contains(name, ".kubebuilder.io") { shortName = strings.TrimSuffix(name, ".kubebuilder.io") } else if idx := strings.LastIndex(name, "."); idx > 0 { // For external plugins, try to get a reasonable short name // Keep the part before the last dot if it looks like a domain parts := strings.Split(name, ".") if len(parts) > 2 { shortName = strings.Join(parts[:len(parts)-1], ".") } } // Strip common suffixes for cleaner display // e.g., "deploy-image.go" -> "deploy-image", "kustomize.common" -> "kustomize" shortName = strings.TrimSuffix(shortName, ".go") shortName = strings.TrimSuffix(shortName, ".common") if version == "" { return shortName } return shortName + "/" + version } // getPluginDescription returns a short description for a plugin key // This is a fallback for plugins that don't implement Describable interface func getPluginDescription(_ string) string { // Fallback for external plugins that don't provide descriptions return "External or custom plugin" } func (c CLI) newRootCmd() *cobra.Command { cmd := &cobra.Command{ Use: c.commandName, Long: c.description, Example: c.rootExamples(), RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, PersistentPreRunE: func(cmd *cobra.Command, _ []string) error { // Check if --plugins flag contains help flags (--help, -h, help) // This handles cases like: kubebuilder init --plugins --help if pluginKeys, err := cmd.Flags().GetStringSlice(pluginsFlag); err == nil { for _, key := range pluginKeys { key = strings.TrimSpace(key) if isHelpFlag(key) { // Help was requested, show help and stop execution cmd.SilenceUsage = true cmd.SilenceErrors = true _ = cmd.Help() return errHelpDisplayed } } } return nil }, } // Global flags for all subcommands. cmd.PersistentFlags().StringSlice(pluginsFlag, nil, "plugin keys to be used for this subcommand execution") // Register --project-version on the root command so that it shows up in help. cmd.Flags().String(projectVersionFlag, c.defaultProjectVersion.String(), "project version") // As the root command will be used to shot the help message under some error conditions, // like during plugin resolving, we need to allow unknown flags to prevent parsing errors. cmd.FParseErrWhitelist = cobra.FParseErrWhitelist{UnknownFlags: true} return cmd } // rootExamples builds the examples string for the root command before resolving plugins func (c CLI) rootExamples() string { str := fmt.Sprintf(`Get started by initializing a new project: %[1]s init --domain The default plugin scaffold includes everything you need. To use optional plugins: %[1]s init --plugins= Available plugins: %[2]s To see which plugins support a specific command: %[1]s --help `, c.commandName, c.getPluginTable()) if len(c.defaultPlugins) != 0 { if defaultPlugins, found := c.defaultPlugins[c.defaultProjectVersion]; found { str += fmt.Sprintf("\nDefault plugin: %q\n", strings.Join(defaultPlugins, ",")) } } return str } // getPluginTable returns an ASCII table of the available plugins and their supported project versions. func (c CLI) getPluginTable() string { return c.getPluginTableFiltered(nil) } // getPluginTableFilteredForSubcommand returns a filtered list of plugins for subcommands, // excluding the default scaffold bundle and its component plugins. func (c CLI) getPluginTableFilteredForSubcommand(filter func(plugin.Plugin) bool) string { return c.getPluginTableFilteredWithOptions(filter, true) } // getPluginTableFiltered returns a formatted list of plugins filtered by a predicate. // If filter is nil, all plugins are included. // Deprecated plugins are automatically excluded from help output. func (c CLI) getPluginTableFiltered(filter func(plugin.Plugin) bool) string { return c.getPluginTableFilteredWithOptions(filter, false) } // getPluginTableFilteredWithOptions returns a formatted list of plugins with filtering options. func (c CLI) getPluginTableFilteredWithOptions(filter func(plugin.Plugin) bool, excludeDefaultScaffold bool) string { type pluginInfo struct { shortKey string fullKey string description string versions string } plugins := make([]pluginInfo, 0, len(c.plugins)) for pluginKey, p := range c.plugins { // Skip deprecated plugins in help output if deprecated, ok := p.(plugin.Deprecated); ok { if deprecated.DeprecationWarning() != "" { continue } } // Apply filter if provided if filter != nil && !filter(p) { continue } // Skip base.go plugin to avoid duplication with go plugin if strings.Contains(pluginKey, "base.go.kubebuilder.io") { continue } // For subcommands, skip default scaffold and its component plugins if excludeDefaultScaffold { if pluginKey == "go.kubebuilder.io/v4" || pluginKey == "kustomize.common.kubebuilder.io/v2" { continue } } shortKey := getShortKey(pluginKey) // Get description from plugin if it implements Describable, otherwise use fallback var desc string if describable, ok := p.(plugin.Describable); ok { desc = describable.Description() } else { desc = getPluginDescription(pluginKey) } // Get supported project versions supportedVersions := p.SupportedProjectVersions() versionStrs := make([]string, 0, len(supportedVersions)) for _, ver := range supportedVersions { versionStrs = append(versionStrs, ver.String()) } versionsStr := strings.Join(versionStrs, ", ") plugins = append(plugins, pluginInfo{ shortKey: shortKey, fullKey: pluginKey, description: desc, versions: versionsStr, }) } if len(plugins) == 0 { return "No plugins available for this subcommand" } // Sort by short key for better readability slices.SortFunc(plugins, func(a, b pluginInfo) int { return strings.Compare(a.shortKey, b.shortKey) }) // Calculate max width for KEY column maxKeyWidth := len("KEY") for _, p := range plugins { if len(p.shortKey) > maxKeyWidth { maxKeyWidth = len(p.shortKey) } } // Build aligned column output lines := make([]string, 0, len(plugins)+1) // Header lines = append(lines, fmt.Sprintf(" %-*s %s", maxKeyWidth, "KEY", "DESCRIPTION")) // Entries for _, p := range plugins { lines = append(lines, fmt.Sprintf(" %-*s %s", maxKeyWidth, p.shortKey, p.description)) } return strings.Join(lines, "\n") } ================================================ FILE: pkg/cli/suite_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) func TestCLI(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "CLI Suite") } // Test plugin types and constructors. var ( _ plugin.Plugin = mockPlugin{} _ plugin.Plugin = mockDeprecatedPlugin{} ) type mockPlugin struct { name string version plugin.Version projectVersions []config.Version } func newMockPlugin(name, version string, projVers ...config.Version) plugin.Plugin { var v plugin.Version if err := v.Parse(version); err != nil { panic(err) } return mockPlugin{name, v, projVers} } func (p mockPlugin) Name() string { return p.name } func (p mockPlugin) Version() plugin.Version { return p.version } func (p mockPlugin) SupportedProjectVersions() []config.Version { return p.projectVersions } type mockDeprecatedPlugin struct { mockPlugin deprecation string } func newMockDeprecatedPlugin(name, version, deprecation string, projVers ...config.Version) plugin.Plugin { return mockDeprecatedPlugin{ mockPlugin: newMockPlugin(name, version, projVers...).(mockPlugin), deprecation: deprecation, } } func (p mockDeprecatedPlugin) DeprecationWarning() string { return p.deprecation } ================================================ FILE: pkg/cli/version.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package cli import ( "fmt" "github.com/spf13/cobra" ) func (c CLI) newVersionCmd() *cobra.Command { cmd := &cobra.Command{ Use: "version", Short: fmt.Sprintf("Print the %s version", c.commandName), Long: fmt.Sprintf("Print the %s version", c.commandName), Example: fmt.Sprintf("%s version", c.commandName), RunE: func(_ *cobra.Command, _ []string) error { fmt.Println(c.version) return nil }, } return cmd } ================================================ FILE: pkg/cli/version_test.go ================================================ /* Copyright 2022 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 cli import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Version", func() { var c *CLI BeforeEach(func() { c = &CLI{} }) Context("newVersionCmd", func() { It("Test the version", func() { cmd := c.newVersionCmd() Expect(cmd).NotTo(BeNil()) Expect(cmd.Use).To(ContainSubstring("version")) Expect(cmd.Use).NotTo(Equal("")) Expect(cmd.Short).NotTo(Equal("")) Expect(cmd.Short).To(ContainSubstring("Print the")) Expect(cmd.Long).NotTo(Equal("")) Expect(cmd.Long).To(ContainSubstring("Print the")) Expect(cmd.Example).NotTo(Equal("")) Expect(cmd.Example).To(ContainSubstring("version")) }) }) }) ================================================ FILE: pkg/cli/webhook.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ //nolint:dupl package cli import ( "fmt" "github.com/spf13/cobra" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) const webhookErrorMsg = "failed to create webhook" func (c CLI) newCreateWebhookCmd() *cobra.Command { cmd := &cobra.Command{ Use: "webhook", Short: "Scaffold a webhook for an API resource", Long: `Scaffold a webhook for an API resource.`, RunE: errCmdFunc( fmt.Errorf("webhook subcommand requires an existing project"), ), } // In case no plugin was resolved, instead of failing the construction of the CLI, fail the execution of // this subcommand. This allows the use of subcommands that do not require resolved plugins like help. if len(c.resolvedPlugins) == 0 { cmdErr(cmd, noResolvedPluginError{}) return cmd } // Obtain the plugin keys and subcommands from the plugins that implement plugin.CreateWebhook. subcommands := c.filterSubcommands( func(p plugin.Plugin) bool { _, isValid := p.(plugin.CreateWebhook) return isValid }, func(p plugin.Plugin) plugin.Subcommand { return p.(plugin.CreateWebhook).GetCreateWebhookSubcommand() }, ) // Verify that there is at least one remaining plugin. if len(subcommands) == 0 { cmdErr(cmd, noAvailablePluginError{"webhook creation"}) return cmd } c.applySubcommandHooks(cmd, subcommands, webhookErrorMsg, false) // Append plugin table after metadata updates c.appendPluginTable(cmd, func(p plugin.Plugin) bool { _, isValid := p.(plugin.CreateWebhook) return isValid }, "Available plugins that support 'create webhook'") return cmd } ================================================ FILE: pkg/cli/webhook_test.go ================================================ /* Copyright 2026 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 cli import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("webhook", func() { Context("constants", func() { It("should have correct error message", func() { Expect(webhookErrorMsg).To(Equal("failed to create webhook")) }) }) }) ================================================ FILE: pkg/config/errors.go ================================================ /* Copyright 2022 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 config import ( "fmt" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) // UnsupportedVersionError is returned by New when a project configuration version is not supported. type UnsupportedVersionError struct { Version Version } // Error implements error interface func (e UnsupportedVersionError) Error() string { return fmt.Sprintf("version %s is not supported", e.Version) } // UnsupportedFieldError is returned when a project configuration version does not support // one of the fields as interface must be common for all the versions type UnsupportedFieldError struct { Version Version Field string } // Error implements error interface func (e UnsupportedFieldError) Error() string { return fmt.Sprintf("version %s does not support the %s field", e.Version, e.Field) } // ResourceNotFoundError is returned by Config.GetResource when the provided GVK cannot be found type ResourceNotFoundError struct { GVK resource.GVK } // Error implements error interface func (e ResourceNotFoundError) Error() string { return fmt.Sprintf("resource %v could not be found", e.GVK) } // PluginKeyNotFoundError is returned by Config.DecodePluginConfig when the provided key cannot be found type PluginKeyNotFoundError struct { Key string } // Error implements error interface func (e PluginKeyNotFoundError) Error() string { return fmt.Sprintf("plugin key %q could not be found", e.Key) } // MarshalError is returned by Config.Marshal when something went wrong while marshalling to YAML type MarshalError struct { Err error } // Error implements error interface func (e MarshalError) Error() string { return fmt.Sprintf("error marshalling project configuration: %v", e.Err) } // Unwrap implements Wrapper interface func (e MarshalError) Unwrap() error { return e.Err } // UnmarshalError is returned by Config.Unmarshal when something went wrong while unmarshalling from YAML type UnmarshalError struct { Err error } // Error implements error interface func (e UnmarshalError) Error() string { return fmt.Sprintf("error unmarshalling project configuration: %v", e.Err) } // Unwrap implements Wrapper interface func (e UnmarshalError) Unwrap() error { return e.Err } ================================================ FILE: pkg/config/errors_test.go ================================================ /* Copyright 2022 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 config import ( "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) var _ = Describe("UnsupportedVersionError", func() { var err UnsupportedVersionError BeforeEach(func() { err = UnsupportedVersionError{ Version: Version{Number: 1}, } }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal("version 1 is not supported")) }) }) }) var _ = Describe("UnsupportedFieldError", func() { var err UnsupportedFieldError BeforeEach(func() { err = UnsupportedFieldError{ Version: Version{Number: 1}, Field: "name", } }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal("version 1 does not support the name field")) }) }) }) var _ = Describe("ResourceNotFoundError", func() { var err ResourceNotFoundError BeforeEach(func() { err = ResourceNotFoundError{ GVK: resource.GVK{ Group: "group", Domain: "my.domain", Version: "v1", Kind: "Kind", }, } }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal("resource {group my.domain v1 Kind} could not be found")) }) }) }) var _ = Describe("PluginKeyNotFoundError", func() { var err PluginKeyNotFoundError BeforeEach(func() { err = PluginKeyNotFoundError{ Key: "go.kubebuilder.io/v1", } }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal("plugin key \"go.kubebuilder.io/v1\" could not be found")) }) }) }) var _ = Describe("MarshalError", func() { var ( wrapped error err MarshalError ) BeforeEach(func() { wrapped = fmt.Errorf("wrapped error") err = MarshalError{Err: wrapped} }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal(fmt.Sprintf("error marshalling project configuration: %v", wrapped))) }) }) Context("Unwrap", func() { It("should unwrap to the wrapped error", func() { Expect(err.Unwrap()).To(Equal(wrapped)) }) }) }) var _ = Describe("UnmarshalError", func() { var ( wrapped error err UnmarshalError ) BeforeEach(func() { wrapped = fmt.Errorf("wrapped error") err = UnmarshalError{Err: wrapped} }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal(fmt.Sprintf("error unmarshalling project configuration: %v", wrapped))) }) }) Context("Unwrap", func() { It("should unwrap to the wrapped error", func() { Expect(err.Unwrap()).To(Equal(wrapped)) }) }) }) ================================================ FILE: pkg/config/interface.go ================================================ /* Copyright 2022 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 config import ( "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) // Config defines the interface that project configuration types must follow. type Config interface { /* Version */ // GetVersion returns the current project version. GetVersion() Version // GetCliVersion returns the CLI binary version that was used to scaffold or initialize the project. GetCliVersion() string // SetCliVersion sets the binary version used to initialize the project. SetCliVersion(version string) error /* String fields */ // GetDomain returns the project domain. GetDomain() string // SetDomain sets the project domain. SetDomain(domain string) error // GetRepository returns the project repository. GetRepository() string // SetRepository sets the project repository. SetRepository(repository string) error // GetProjectName returns the project name. // This method was introduced in project version 3. GetProjectName() string // SetProjectName sets the project name. // This method was introduced in project version 3. SetProjectName(name string) error // GetPluginChain returns the plugin chain. // This method was introduced in project version 3. GetPluginChain() []string // SetPluginChain sets the plugin chain. // This method was introduced in project version 3. SetPluginChain(pluginChain []string) error /* Boolean fields */ // IsMultiGroup checks if multi-group is enabled. IsMultiGroup() bool // SetMultiGroup enables multi-group. SetMultiGroup() error // ClearMultiGroup disables multi-group. ClearMultiGroup() error // IsNamespaced checks if the project is configured for namespace-scoped deployment. IsNamespaced() bool // SetNamespaced enables namespace-scoped deployment. SetNamespaced() error // ClearNamespaced disables namespace-scoped deployment (default: cluster-scoped). ClearNamespaced() error /* Resources */ // ResourcesLength returns the number of tracked resources. ResourcesLength() int // HasResource checks if the provided GVK is stored in the Config. HasResource(gvk resource.GVK) bool // GetResource returns the stored resource matching the provided GVK. GetResource(gvk resource.GVK) (resource.Resource, error) // GetResources returns all the stored resources. GetResources() ([]resource.Resource, error) // AddResource adds the provided resource if it was not present, no-op if it was already present. AddResource(res resource.Resource) error // UpdateResource adds the provided resource if it was not present, modifies it if it was already present. UpdateResource(res resource.Resource) error // HasGroup checks if the provided group is the same as any of the tracked resources. HasGroup(group string) bool // ListCRDVersions returns a list of the CRD versions in use by the tracked resources. ListCRDVersions() []string // ListWebhookVersions returns a list of the webhook versions in use by the tracked resources. ListWebhookVersions() []string /* Plugins */ // DecodePluginConfig decodes a plugin config stored in Config into configObj, which must be a pointer. // This method is intended to be used for custom configuration objects, which were introduced in project version 3. DecodePluginConfig(key string, configObj any) error // EncodePluginConfig encodes a config object into Config by overwriting the existing object stored under key. // This method is intended to be used for custom configuration objects, which were introduced in project version 3. EncodePluginConfig(key string, configObj any) error /* Persistence */ // MarshalYAML Marshal returns the YAML representation of the Config. MarshalYAML() ([]byte, error) // UnmarshalYAML Unmarshal loads the Config fields from its YAML representation. UnmarshalYAML([]byte) error } ================================================ FILE: pkg/config/registry.go ================================================ /* Copyright 2022 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 config var registry = make(map[Version]func() Config) // Register allows implementations of Config to register themselves so that they can be created with New func Register(version Version, constructor func() Config) { registry[version] = constructor } // IsRegistered returns true if the given version has been registered through Register func IsRegistered(version Version) bool { _, ok := registry[version] return ok } // New creates Config instances from the previously registered implementations through Register func New(version Version) (Config, error) { if constructor, exists := registry[version]; exists { return constructor(), nil } return nil, UnsupportedVersionError{Version: version} } ================================================ FILE: pkg/config/registry_test.go ================================================ /* Copyright 2022 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 config import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("registry", func() { var ( version Version f func() Config ) BeforeEach(func() { version = Version{} f = func() Config { return nil } }) AfterEach(func() { registry = make(map[Version]func() Config) }) Context("Register", func() { It("should register new constructors", func() { Register(version, f) Expect(registry).To(HaveKey(version)) Expect(registry[version]()).To(BeNil()) }) }) Context("IsRegistered", func() { It("should return true for registered constructors", func() { Register(version, f) Expect(IsRegistered(version)).To(BeTrue()) }) It("should fail for unregistered constructors", func() { Expect(IsRegistered(version)).To(BeFalse()) }) }) Context("New", func() { It("should use the registered constructors", func() { registry[version] = f result, err := New(version) Expect(err).NotTo(HaveOccurred()) Expect(result).To(BeNil()) }) It("should fail for unregistered constructors", func() { _, err := New(version) Expect(err).To(HaveOccurred()) }) }) }) ================================================ FILE: pkg/config/store/errors.go ================================================ /* Copyright 2022 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 store import ( "fmt" ) // LoadError wraps errors yielded by Store.Load and Store.LoadFrom methods type LoadError struct { Err error } // Error implements error interface func (e LoadError) Error() string { return fmt.Sprintf("unable to load the configuration: %v", e.Err) } // Unwrap implements Wrapper interface func (e LoadError) Unwrap() error { return e.Err } // SaveError wraps errors yielded by Store.Save and Store.SaveTo methods type SaveError struct { Err error } // Error implements error interface func (e SaveError) Error() string { return fmt.Sprintf("unable to save the configuration: %v", e.Err) } // Unwrap implements Wrapper interface func (e SaveError) Unwrap() error { return e.Err } ================================================ FILE: pkg/config/store/errors_test.go ================================================ /* Copyright 2022 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 store import ( "fmt" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestConfigStore(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Config Store Suite") } var _ = Describe("LoadError", func() { var ( wrapped error err LoadError ) BeforeEach(func() { wrapped = fmt.Errorf("error message") err = LoadError{Err: wrapped} }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal(fmt.Sprintf("unable to load the configuration: %v", wrapped))) }) }) Context("Unwrap", func() { It("should unwrap to the wrapped error", func() { Expect(err.Unwrap()).To(Equal(wrapped)) }) }) }) var _ = Describe("SaveError", func() { var ( wrapped error err SaveError ) BeforeEach(func() { wrapped = fmt.Errorf("error message") err = SaveError{Err: wrapped} }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal(fmt.Sprintf("unable to save the configuration: %v", wrapped))) }) }) Context("Unwrap", func() { It("should unwrap to the wrapped error", func() { Expect(err.Unwrap()).To(Equal(wrapped)) }) }) }) ================================================ FILE: pkg/config/store/interface.go ================================================ /* Copyright 2022 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 store import ( "sigs.k8s.io/kubebuilder/v4/pkg/config" ) // Store represents a persistence backend for config.Config type Store interface { // New creates a new config.Config to store New(config.Version) error // Load retrieves the config.Config from the persistence backend Load() error // LoadFrom retrieves the config.Config from the persistence backend at the specified key LoadFrom(string) error // Save stores the config.Config into the persistence backend Save() error // SaveTo stores the config.Config into the persistence backend at the specified key SaveTo(string) error // Config returns the stored config.Config Config() config.Config } ================================================ FILE: pkg/config/store/yaml/store.go ================================================ /* Copyright 2022 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 yaml import ( "fmt" "os" "github.com/spf13/afero" "sigs.k8s.io/yaml" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) const ( // DefaultPath is the default path for the configuration file DefaultPath = "PROJECT" // Comment for 'PROJECT' config file commentStr = `# Code generated by tool. DO NOT EDIT. # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html ` ) // yamlStore implements store.Store using a YAML file as the storage backend // The key is translated into the YAML file path type yamlStore struct { // fs is the filesystem that will be used to store the config.Config fs afero.Fs // mustNotExist requires the file not to exist when saving it mustNotExist bool cfg config.Config } // New creates a new configuration that will be stored at the provided path func New(fs machinery.Filesystem) store.Store { return &yamlStore{fs: fs.FS} } // New implements store.Store interface func (s *yamlStore) New(version config.Version) error { cfg, err := config.New(version) if err != nil { return fmt.Errorf("could not create config: %w", err) } s.cfg = cfg s.mustNotExist = true return nil } // Load implements store.Store interface func (s *yamlStore) Load() error { return s.LoadFrom(DefaultPath) } type versionedConfig struct { Version config.Version `json:"version"` } // LoadFrom implements store.Store interface func (s *yamlStore) LoadFrom(path string) error { s.mustNotExist = false // Read the file in, err := afero.ReadFile(s.fs, path) if err != nil { return store.LoadError{Err: fmt.Errorf("failed to read %q file: %w", path, err)} } // Check the file version var versioned versionedConfig if err = yaml.Unmarshal(in, &versioned); err != nil { return store.LoadError{Err: fmt.Errorf("failed to determine config version: %w", err)} } // Create the config object var cfg config.Config cfg, err = config.New(versioned.Version) if err != nil { return store.LoadError{Err: fmt.Errorf("failed to create config for version %q: %w", versioned.Version, err)} } // Unmarshal the file content if err := cfg.UnmarshalYAML(in); err != nil { return store.LoadError{Err: fmt.Errorf("failed to unmarshal config at %q: %w", path, err)} } s.cfg = cfg return nil } // Save implements store.Store interface func (s yamlStore) Save() error { return s.SaveTo(DefaultPath) } // SaveTo implements store.Store interface func (s yamlStore) SaveTo(path string) error { // If yamlStore is unset, none of New, Load, or LoadFrom were called successfully if s.cfg == nil { return store.SaveError{Err: fmt.Errorf("undefined config, use one of the initializers: New, Load, LoadFrom")} } // If it is a new configuration, the path should not exist yet if s.mustNotExist { // Check that the file doesn't exist _, err := s.fs.Stat(path) if err == nil || os.IsExist(err) { // File already exists return store.SaveError{Err: fmt.Errorf("configuration already exists in %q", path)} } else if !os.IsNotExist(err) { // Error occurred while checking file existence return store.SaveError{Err: fmt.Errorf("failed to check for file prior existence: %w", err)} } } // Marshall into YAML content, err := s.cfg.MarshalYAML() if err != nil { return store.SaveError{Err: fmt.Errorf("failed to marshal to YAML: %w", err)} } // Prepend warning comment for the 'PROJECT' file content = append([]byte(commentStr), content...) // Write the marshalled configuration err = afero.WriteFile(s.fs, path, content, machinery.DefaultFilePermission) if err != nil { return store.SaveError{Err: fmt.Errorf("failed to save configuration to %q: %w", path, err)} } return nil } // Config implements store.Store interface func (s yamlStore) Config() config.Config { return s.cfg } ================================================ FILE: pkg/config/store/yaml/store_test.go ================================================ /* Copyright 2022 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 yaml import ( "errors" "fmt" "os" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/afero" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/config/store" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) func TestConfigStoreYaml(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Config Store YAML Suite") } var _ = Describe("New", func() { It("should return a new empty store", func() { s := New(machinery.Filesystem{FS: afero.NewMemMapFs()}) Expect(s.Config()).To(BeNil()) ys, ok := s.(*yamlStore) Expect(ok).To(BeTrue()) Expect(ys.fs).NotTo(BeNil()) }) }) var _ = Describe("yamlStore", func() { const ( v3File = `version: "3" ` unversionedFile = `version: ` nonexistentVersionFile = `version: 1-alpha ` // v1-alpha never existed wrongFile = `version: "2" layout: "" ` // layout field does not exist in v2 ) var ( s *yamlStore path string ) BeforeEach(func() { s = New(machinery.Filesystem{FS: afero.NewMemMapFs()}).(*yamlStore) path = DefaultPath + "2" }) Context("New", func() { It("should fail for an unregistered config version", func() { Expect(s.New(config.Version{})).NotTo(Succeed()) }) }) Context("Load", func() { It("should load the Config from an existing file at the default path", func() { Expect(afero.WriteFile(s.fs, DefaultPath, []byte(commentStr+v3File), os.ModePerm)).To(Succeed()) Expect(s.Load()).To(Succeed()) Expect(s.fs).NotTo(BeNil()) Expect(s.mustNotExist).To(BeFalse()) Expect(s.Config()).NotTo(BeNil()) Expect(s.Config().GetVersion().Compare(cfgv3.Version)).To(Equal(0)) }) It("should fail if no file exists at the default path", func() { err := s.Load() Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to read %q file: %w", DefaultPath, &os.PathError{ Err: os.ErrNotExist, Path: DefaultPath, Op: "open", }), })) }) It("should fail if unable to identify the version of the file at the default path", func() { Expect(afero.WriteFile(s.fs, DefaultPath, []byte(commentStr+unversionedFile), os.ModePerm)).To(Succeed()) err := s.Load() Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to determine config version: %w", fmt.Errorf("error unmarshaling JSON: %w", fmt.Errorf("while decoding JSON: %w", errors.New("project version is empty"), ), ), ), })) }) It("should fail if unable to create a Config for the version of the file at the default path", func() { Expect(afero.WriteFile(s.fs, DefaultPath, []byte(commentStr+nonexistentVersionFile), os.ModePerm)).To(Succeed()) err := s.Load() Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to create config for version %q: %w", "1-alpha", config.UnsupportedVersionError{ Version: config.Version{Number: 1, Stage: 2}, }), })) }) It("should fail if unable to unmarshal the file at the default path", func() { Expect(afero.WriteFile(s.fs, DefaultPath, []byte(commentStr+wrongFile), os.ModePerm)).To(Succeed()) err := s.Load() Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to create config for version %q: %w", "2", config.UnsupportedVersionError{ Version: config.Version{ Number: 2, Stage: 0, }, }), })) }) }) Context("LoadFrom", func() { It("should load the Config from an existing file from the specified path", func() { Expect(afero.WriteFile(s.fs, path, []byte(commentStr+v3File), os.ModePerm)).To(Succeed()) Expect(s.LoadFrom(path)).To(Succeed()) Expect(s.fs).NotTo(BeNil()) Expect(s.mustNotExist).To(BeFalse()) Expect(s.Config()).NotTo(BeNil()) Expect(s.Config().GetVersion().Compare(cfgv3.Version)).To(Equal(0)) }) It("should fail if no file exists at the specified path", func() { err := s.LoadFrom(path) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to read %q file: %w", path, &os.PathError{ Err: os.ErrNotExist, Path: path, Op: "open", }), })) }) It("should fail if unable to identify the version of the file at the specified path", func() { Expect(afero.WriteFile(s.fs, path, []byte(commentStr+unversionedFile), os.ModePerm)).To(Succeed()) err := s.LoadFrom(path) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to determine config version: %w", fmt.Errorf("error unmarshaling JSON: %w", fmt.Errorf("while decoding JSON: %w", errors.New("project version is empty"), ), ), ), })) }) It("should fail if unable to create a Config for the version of the file at the specified path", func() { Expect(afero.WriteFile(s.fs, path, []byte(commentStr+nonexistentVersionFile), os.ModePerm)).To(Succeed()) err := s.LoadFrom(path) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to create config for version %q: %w", "1-alpha", config.UnsupportedVersionError{ Version: config.Version{Number: 1, Stage: 2}, }), })) }) It("should fail if unable to unmarshal the file at the specified path", func() { Expect(afero.WriteFile(s.fs, path, []byte(commentStr+wrongFile), os.ModePerm)).To(Succeed()) err := s.LoadFrom(path) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.LoadError{ Err: fmt.Errorf("failed to create config for version %q: %w", "2", config.UnsupportedVersionError{ Version: config.Version{ Number: 2, }, }), })) }) }) Context("Save", func() { It("should succeed for a valid config", func() { s.cfg = cfgv3.New() Expect(s.Save()).To(Succeed()) cfgBytes, err := afero.ReadFile(s.fs, DefaultPath) Expect(err).NotTo(HaveOccurred()) Expect(string(cfgBytes)).To(Equal(commentStr + v3File)) }) It("should succeed for a valid config that must not exist", func() { s.cfg = cfgv3.New() s.mustNotExist = true Expect(s.Save()).To(Succeed()) cfgBytes, err := afero.ReadFile(s.fs, DefaultPath) Expect(err).NotTo(HaveOccurred()) Expect(string(cfgBytes)).To(Equal(commentStr + v3File)) }) It("should fail for an empty config", func() { err := s.Save() Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.SaveError{ Err: errors.New("undefined config, use one of the initializers: New, Load, LoadFrom"), })) }) It("should fail for a pre-existent file that must not exist", func() { s.cfg = cfgv3.New() s.mustNotExist = true Expect(afero.WriteFile(s.fs, DefaultPath, []byte(v3File), os.ModePerm)).To(Succeed()) err := s.Save() Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.SaveError{ Err: fmt.Errorf("configuration already exists in %q", DefaultPath), })) }) }) Context("SaveTo", func() { It("should success for valid configs", func() { s.cfg = cfgv3.New() Expect(s.SaveTo(path)).To(Succeed()) cfgBytes, err := afero.ReadFile(s.fs, path) Expect(err).NotTo(HaveOccurred()) Expect(string(cfgBytes)).To(Equal(commentStr + v3File)) }) It("should succeed for a valid config that must not exist", func() { s.cfg = cfgv3.New() s.mustNotExist = true Expect(s.SaveTo(path)).To(Succeed()) cfgBytes, err := afero.ReadFile(s.fs, path) Expect(err).NotTo(HaveOccurred()) Expect(string(cfgBytes)).To(Equal(commentStr + v3File)) }) It("should fail for an empty config", func() { err := s.SaveTo(path) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.SaveError{ Err: errors.New("undefined config, use one of the initializers: New, Load, LoadFrom"), })) }) It("should fail for a pre-existent file that must not exist", func() { s.cfg = cfgv3.New() s.mustNotExist = true Expect(afero.WriteFile(s.fs, path, []byte(v3File), os.ModePerm)).To(Succeed()) err := s.SaveTo(path) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(store.SaveError{ Err: fmt.Errorf("configuration already exists in %q", path), })) }) }) }) ================================================ FILE: pkg/config/suite_test.go ================================================ /* Copyright 2022 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 config import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestConfig(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Config Suite") } ================================================ FILE: pkg/config/v3/config.go ================================================ /* Copyright 2022 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 v3 import ( "fmt" "strings" "sigs.k8s.io/yaml" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) // Version is the config.Version for project configuration 3 var Version = config.Version{Number: 3} // stringSlice is a []string but that can also be unmarshalled from a single string, // which is introduced as the first and only element of the slice // It is used to offer backwards compatibility as the field used to be a string. type stringSlice []string func (ss *stringSlice) UnmarshalJSON(b []byte) error { if b[0] == '[' { var sl []string if err := yaml.Unmarshal(b, &sl); err != nil { return fmt.Errorf("error unmarshalling string slice %q: %w", sl, err) } *ss = sl return nil } var st string if err := yaml.Unmarshal(b, &st); err != nil { return fmt.Errorf("error unmarshalling string %q: %w", st, err) } *ss = stringSlice{st} return nil } // Cfg defines the Project Config (PROJECT file) type Cfg struct { // Version Version config.Version `json:"version"` // String fields Domain string `json:"domain,omitempty"` Repository string `json:"repo,omitempty"` Name string `json:"projectName,omitempty"` CliVersion string `json:"cliVersion,omitempty"` PluginChain stringSlice `json:"layout,omitempty"` // Boolean fields MultiGroup bool `json:"multigroup,omitempty"` Namespaced bool `json:"namespaced,omitempty"` // Resources Resources []resource.Resource `json:"resources,omitempty"` // Plugins Plugins pluginConfigs `json:"plugins,omitempty"` } // pluginConfigs holds a set of arbitrary plugin configuration objects mapped by plugin key. type pluginConfigs map[string]pluginConfig // pluginConfig is an arbitrary plugin configuration object. type pluginConfig any // New returns a new config.Config func New() config.Config { return &Cfg{Version: Version} } func init() { config.Register(Version, New) } // GetVersion implements config.Config func (c Cfg) GetVersion() config.Version { return c.Version } // GetCliVersion implements config.Config func (c Cfg) GetCliVersion() string { return c.CliVersion } // SetCliVersion implements config.Config func (c *Cfg) SetCliVersion(version string) error { c.CliVersion = version return nil } // GetDomain implements config.Config func (c Cfg) GetDomain() string { return c.Domain } // SetDomain implements config.Config func (c *Cfg) SetDomain(domain string) error { c.Domain = domain return nil } // GetRepository implements config.Config func (c Cfg) GetRepository() string { return c.Repository } // SetRepository implements config.Config func (c *Cfg) SetRepository(repository string) error { c.Repository = repository return nil } // GetProjectName implements config.Config func (c Cfg) GetProjectName() string { return c.Name } // SetProjectName implements config.Config func (c *Cfg) SetProjectName(name string) error { c.Name = name return nil } // GetPluginChain implements config.Config func (c Cfg) GetPluginChain() []string { return c.PluginChain } // SetPluginChain implements config.Config func (c *Cfg) SetPluginChain(pluginChain []string) error { c.PluginChain = pluginChain return nil } // IsMultiGroup implements config.Config func (c Cfg) IsMultiGroup() bool { return c.MultiGroup } // SetMultiGroup implements config.Config func (c *Cfg) SetMultiGroup() error { c.MultiGroup = true return nil } // ClearMultiGroup implements config.Config func (c *Cfg) ClearMultiGroup() error { c.MultiGroup = false return nil } // IsNamespaced implements config.Config func (c Cfg) IsNamespaced() bool { return c.Namespaced } // SetNamespaced implements config.Config func (c *Cfg) SetNamespaced() error { c.Namespaced = true return nil } // ClearNamespaced implements config.Config func (c *Cfg) ClearNamespaced() error { c.Namespaced = false return nil } // ResourcesLength implements config.Config func (c Cfg) ResourcesLength() int { return len(c.Resources) } // HasResource implements config.Config func (c Cfg) HasResource(gvk resource.GVK) bool { found := false for _, res := range c.Resources { if gvk.IsEqualTo(res.GVK) { found = true break } } return found } // GetResource implements config.Config func (c Cfg) GetResource(gvk resource.GVK) (resource.Resource, error) { for _, res := range c.Resources { if gvk.IsEqualTo(res.GVK) { r := res.Copy() // Plural is only stored if irregular, so if it is empty recover the regular form if r.Plural == "" { r.Plural = resource.RegularPlural(r.Kind) } return r, nil } } return resource.Resource{}, config.ResourceNotFoundError{GVK: gvk} } // GetResources implements config.Config func (c Cfg) GetResources() ([]resource.Resource, error) { resources := make([]resource.Resource, 0, len(c.Resources)) for _, res := range c.Resources { r := res.Copy() // Plural is only stored if irregular, so if it is empty recover the regular form if r.Plural == "" { r.Plural = resource.RegularPlural(r.Kind) } resources = append(resources, r) } return resources, nil } // AddResource implements config.Config func (c *Cfg) AddResource(res resource.Resource) error { // As res is passed by value it is already a shallow copy, but we need to make a deep copy res = res.Copy() // Plural is only stored if irregular if res.Plural == resource.RegularPlural(res.Kind) { res.Plural = "" } if !c.HasResource(res.GVK) { c.Resources = append(c.Resources, res) } return nil } // UpdateResource implements config.Config func (c *Cfg) UpdateResource(res resource.Resource) error { // As res is passed by value it is already a shallow copy, but we need to make a deep copy res = res.Copy() // Plural is only stored if irregular if res.Plural == resource.RegularPlural(res.Kind) { res.Plural = "" } for i, r := range c.Resources { if res.IsEqualTo(r.GVK) { if err := c.Resources[i].Update(res); err != nil { return fmt.Errorf("failed to update resource %q: %w", res.GVK, err) } return nil } } c.Resources = append(c.Resources, res) return nil } // HasGroup implements config.Config func (c Cfg) HasGroup(group string) bool { // Return true if the target group is found in the tracked resources for _, r := range c.Resources { if strings.EqualFold(group, r.Group) { return true } } // Return false otherwise return false } // ListCRDVersions implements config.Config func (c Cfg) ListCRDVersions() []string { // Make a map to remove duplicates versionSet := make(map[string]struct{}) for _, r := range c.Resources { if r.API != nil && r.API.CRDVersion != "" { versionSet[r.API.CRDVersion] = struct{}{} } } // Convert the map into a slice versions := make([]string, 0, len(versionSet)) for version := range versionSet { versions = append(versions, version) } return versions } // ListWebhookVersions implements config.Config func (c Cfg) ListWebhookVersions() []string { // Make a map to remove duplicates versionSet := make(map[string]struct{}) for _, r := range c.Resources { if r.Webhooks != nil && r.Webhooks.WebhookVersion != "" { versionSet[r.Webhooks.WebhookVersion] = struct{}{} } } // Convert the map into a slice versions := make([]string, 0, len(versionSet)) for version := range versionSet { versions = append(versions, version) } return versions } // DecodePluginConfig implements config.Config func (c Cfg) DecodePluginConfig(key string, configObj any) error { if len(c.Plugins) == 0 { return config.PluginKeyNotFoundError{Key: key} } // Get the object blob by key and unmarshal into the object. if pluginCfg, hasKey := c.Plugins[key]; hasKey { b, err := yaml.Marshal(pluginCfg) if err != nil { return fmt.Errorf("failed to convert extra fields object to bytes: %w", err) } if err := yaml.Unmarshal(b, configObj); err != nil { return fmt.Errorf("failed to unmarshal extra fields object: %w", err) } return nil } return config.PluginKeyNotFoundError{Key: key} } // EncodePluginConfig will return an error if used on any project version < v3. func (c *Cfg) EncodePluginConfig(key string, configObj any) error { // Get object's bytes and set them under key in extra fields. b, err := yaml.Marshal(configObj) if err != nil { return fmt.Errorf("failed to convert %T object to bytes: %w", configObj, err) } var fields map[string]any if err := yaml.Unmarshal(b, &fields); err != nil { return fmt.Errorf("failed to unmarshal %T object bytes: %w", configObj, err) } if c.Plugins == nil { c.Plugins = make(map[string]pluginConfig) } c.Plugins[key] = fields return nil } // MarshalYAML implements config.Config func (c Cfg) MarshalYAML() ([]byte, error) { for i, r := range c.Resources { // If API is empty, omit it (prevents `api: {}`). if r.API != nil && r.API.IsEmpty() { c.Resources[i].API = nil } // If Webhooks is empty, omit it (prevents `webhooks: {}`). if r.Webhooks != nil && r.Webhooks.IsEmpty() { c.Resources[i].Webhooks = nil } } content, err := yaml.Marshal(c) if err != nil { return nil, config.MarshalError{Err: err} } return content, nil } // UnmarshalYAML implements config.Config func (c *Cfg) UnmarshalYAML(b []byte) error { // Use non-strict unmarshaling to allow forward compatibility and external plugin fields. // Older versions of kubebuilder should be able to read PROJECT files // with newer fields and simply ignore unknown fields. if err := yaml.Unmarshal(b, c); err != nil { return config.UnmarshalError{Err: err} } return nil } ================================================ FILE: pkg/config/v3/config_test.go ================================================ /* Copyright 2022 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 v3 import ( "slices" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) func TestConfigV3(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Config V3 Suite") } var _ = Describe("Cfg", func() { const ( domain = "my.domain" repo = "myrepo" name = "ProjectName" otherDomain = "other.domain" otherRepo = "otherrepo" otherName = "OtherProjectName" ) var ( c Cfg pluginChain []string otherPluginChain []string ) BeforeEach(func() { pluginChain = []string{"go.kubebuilder.io/v2"} otherPluginChain = []string{"go.kubebuilder.io/v3"} c = Cfg{ Version: Version, Domain: domain, Repository: repo, Name: name, PluginChain: pluginChain, } }) Context("Version", func() { It("GetVersion should return version 3", func() { Expect(c.GetVersion().Compare(Version)).To(Equal(0)) }) }) Context("Domain", func() { It("GetDomain should return the domain", func() { Expect(c.GetDomain()).To(Equal(domain)) }) It("SetDomain should set the domain", func() { Expect(c.SetDomain(otherDomain)).To(Succeed()) Expect(c.Domain).To(Equal(otherDomain)) }) }) Context("Repository", func() { It("GetRepository should return the repository", func() { Expect(c.GetRepository()).To(Equal(repo)) }) It("SetRepository should set the repository", func() { Expect(c.SetRepository(otherRepo)).To(Succeed()) Expect(c.Repository).To(Equal(otherRepo)) }) }) Context("Project name", func() { It("GetProjectName should return the name", func() { Expect(c.GetProjectName()).To(Equal(name)) }) It("SetProjectName should set the name", func() { Expect(c.SetProjectName(otherName)).To(Succeed()) Expect(c.Name).To(Equal(otherName)) }) }) Context("Plugin chain", func() { It("GetPluginChain should return the plugin chain", func() { Expect(c.GetPluginChain()).To(Equal(pluginChain)) }) It("SetPluginChain should set the plugin chain", func() { Expect(c.SetPluginChain(otherPluginChain)).To(Succeed()) Expect([]string(c.PluginChain)).To(Equal(otherPluginChain)) }) }) Context("Multi group", func() { It("IsMultiGroup should return false if not set", func() { Expect(c.IsMultiGroup()).To(BeFalse()) }) It("IsMultiGroup should return true if set", func() { c.MultiGroup = true Expect(c.IsMultiGroup()).To(BeTrue()) }) It("SetMultiGroup should enable multi-group support", func() { Expect(c.SetMultiGroup()).To(Succeed()) Expect(c.MultiGroup).To(BeTrue()) }) It("ClearMultiGroup should disable multi-group support", func() { c.MultiGroup = true Expect(c.ClearMultiGroup()).To(Succeed()) Expect(c.MultiGroup).To(BeFalse()) }) }) Context("Resources", func() { var ( res resource.Resource resWithoutPlural resource.Resource checkResource func(result, expected resource.Resource) ) BeforeEach(func() { res = resource.Resource{ GVK: resource.GVK{ Group: "group", Version: "v1", Kind: "Kind", }, Plural: "kinds", Path: "api/v1", API: &resource.API{ CRDVersion: "v1", Namespaced: true, }, Controller: true, Webhooks: &resource.Webhooks{ WebhookVersion: "v1", Defaulting: true, Validation: true, Conversion: true, }, } resWithoutPlural = res.Copy() // As some of the tests insert directly into the slice without using the interface methods, // regular plural forms should not be present in here. rsWithoutPlural is used for this purpose. resWithoutPlural.Plural = "" // Auxiliary function for GetResource, AddResource and UpdateResource tests checkResource = func(result, expected resource.Resource) { Expect(result.GVK.IsEqualTo(expected.GVK)).To(BeTrue()) Expect(result.Plural).To(Equal(expected.Plural)) Expect(result.Path).To(Equal(expected.Path)) if expected.API == nil { Expect(result.API).To(BeNil()) } else { Expect(result.API).NotTo(BeNil()) Expect(result.API.CRDVersion).To(Equal(expected.API.CRDVersion)) Expect(result.API.Namespaced).To(Equal(expected.API.Namespaced)) } Expect(result.Controller).To(Equal(expected.Controller)) if expected.Webhooks == nil { Expect(result.Webhooks).To(BeNil()) } else { Expect(result.Webhooks).NotTo(BeNil()) Expect(result.Webhooks.WebhookVersion).To(Equal(expected.Webhooks.WebhookVersion)) Expect(result.Webhooks.Defaulting).To(Equal(expected.Webhooks.Defaulting)) Expect(result.Webhooks.Validation).To(Equal(expected.Webhooks.Validation)) Expect(result.Webhooks.Conversion).To(Equal(expected.Webhooks.Conversion)) } } }) DescribeTable("ResourcesLength should return the number of resources", func(n int) { for range n { c.Resources = append(c.Resources, resWithoutPlural) } Expect(c.ResourcesLength()).To(Equal(n)) }, Entry("for no resources", 0), Entry("for one resource", 1), Entry("for several resources", 3), ) It("HasResource should return false for a non-existent resource", func() { Expect(c.HasResource(res.GVK)).To(BeFalse()) }) It("HasResource should return true for an existent resource", func() { c.Resources = append(c.Resources, resWithoutPlural) Expect(c.HasResource(res.GVK)).To(BeTrue()) }) It("GetResource should fail for a non-existent resource", func() { _, err := c.GetResource(res.GVK) Expect(err).To(HaveOccurred()) }) It("GetResource should return an existent resource", func() { c.Resources = append(c.Resources, resWithoutPlural) r, err := c.GetResource(res.GVK) Expect(err).NotTo(HaveOccurred()) checkResource(r, res) }) It("GetResources should return a slice of the tracked resources", func() { c.Resources = append(c.Resources, resWithoutPlural, resWithoutPlural, resWithoutPlural) resources, err := c.GetResources() Expect(err).NotTo(HaveOccurred()) Expect(resources).To(Equal([]resource.Resource{res, res, res})) }) It("AddResource should add the provided resource if non-existent", func() { l := len(c.Resources) Expect(c.AddResource(res)).To(Succeed()) Expect(c.Resources).To(HaveLen(l + 1)) checkResource(c.Resources[0], resWithoutPlural) }) It("AddResource should do nothing if the resource already exists", func() { c.Resources = append(c.Resources, res) l := len(c.Resources) Expect(c.AddResource(res)).To(Succeed()) Expect(c.Resources).To(HaveLen(l)) }) It("UpdateResource should add the provided resource if non-existent", func() { l := len(c.Resources) Expect(c.UpdateResource(res)).To(Succeed()) Expect(c.Resources).To(HaveLen(l + 1)) checkResource(c.Resources[0], resWithoutPlural) }) It("UpdateResource should update it if the resource already exists", func() { r := resource.Resource{ GVK: resource.GVK{ Group: "group", Version: "v1", Kind: "Kind", }, Path: "api/v1", } c.Resources = append(c.Resources, r) l := len(c.Resources) checkResource(c.Resources[0], r) Expect(c.UpdateResource(res)).To(Succeed()) Expect(c.Resources).To(HaveLen(l)) checkResource(c.Resources[0], resWithoutPlural) }) It("HasGroup should return false with no tracked resources", func() { Expect(c.HasGroup(res.Group)).To(BeFalse()) }) It("HasGroup should return true with tracked resources in the same group", func() { c.Resources = append(c.Resources, res) Expect(c.HasGroup(res.Group)).To(BeTrue()) }) It("HasGroup should return false with tracked resources in other group", func() { c.Resources = append(c.Resources, res) Expect(c.HasGroup("other-group")).To(BeFalse()) }) It("ListCRDVersions should return an empty list with no tracked resources", func() { Expect(c.ListCRDVersions()).To(BeEmpty()) }) It("ListCRDVersions should return a list of tracked resources CRD versions", func() { c.Resources = append(c.Resources, resource.Resource{ GVK: resource.GVK{ Group: res.Group, Version: res.Version, Kind: res.Kind, }, API: &resource.API{CRDVersion: "v1beta1"}, }, resource.Resource{ GVK: resource.GVK{ Group: res.Group, Version: res.Version, Kind: "OtherKind", }, API: &resource.API{CRDVersion: "v1"}, }, ) versions := c.ListCRDVersions() slices.Sort(versions) // ListCRDVersions has no order guarantee so sorting for reproducibility Expect(versions).To(Equal([]string{"v1", "v1beta1"})) }) It("ListWebhookVersions should return an empty list with no tracked resources", func() { Expect(c.ListWebhookVersions()).To(BeEmpty()) }) It("ListWebhookVersions should return a list of tracked resources webhook versions", func() { c.Resources = append(c.Resources, resource.Resource{ GVK: resource.GVK{ Group: res.Group, Version: res.Version, Kind: res.Kind, }, Webhooks: &resource.Webhooks{WebhookVersion: "v1beta1"}, }, resource.Resource{ GVK: resource.GVK{ Group: res.Group, Version: res.Version, Kind: "OtherKind", }, Webhooks: &resource.Webhooks{WebhookVersion: "v1"}, }, ) versions := c.ListWebhookVersions() slices.Sort(versions) // ListWebhookVersions has no order guarantee so sorting for reproducibility Expect(versions).To(Equal([]string{"v1", "v1beta1"})) }) }) Context("Plugins", func() { // Test plugin config. Don't want to export this config, but need it to // be accessible by test. type PluginConfig struct { Data1 string `json:"data-1"` Data2 string `json:"data-2,omitempty"` } const ( key = "plugin-x" ) var ( c0, c1, c2 Cfg pluginCfg PluginConfig ) BeforeEach(func() { c0 = Cfg{ Version: Version, Domain: domain, Repository: repo, Name: name, PluginChain: pluginChain, } c1 = Cfg{ Version: Version, Domain: domain, Repository: repo, Name: name, PluginChain: pluginChain, Plugins: pluginConfigs{ key: map[string]any{ "data-1": "", }, }, } c2 = Cfg{ Version: Version, Domain: domain, Repository: repo, Name: name, PluginChain: pluginChain, Plugins: pluginConfigs{ key: map[string]any{ "data-1": "plugin value 1", "data-2": "plugin value 2", }, }, } pluginCfg = PluginConfig{ Data1: "plugin value 1", Data2: "plugin value 2", } }) It("DecodePluginConfig should fail for no plugin config object", func() { err := c0.DecodePluginConfig(key, &pluginCfg) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(config.PluginKeyNotFoundError{Key: key})) }) It("DecodePluginConfig should fail to retrieve data from a non-existent plugin", func() { err := c1.DecodePluginConfig("plugin-y", &pluginCfg) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(config.PluginKeyNotFoundError{Key: "plugin-y"})) }) DescribeTable("DecodePluginConfig should retrieve the plugin data correctly", func(getCfg func() Cfg, expected func() PluginConfig) { pluginCfg = PluginConfig{} // reset to not reuse values Expect(getCfg().DecodePluginConfig(key, &pluginCfg)).To(Succeed()) Expect(pluginCfg).To(Equal(expected())) }, Entry("for an empty plugin config object", func() Cfg { return c1 }, func() PluginConfig { return PluginConfig{} }), Entry("for a full plugin config object", func() Cfg { return c2 }, func() PluginConfig { return pluginCfg }), // TODO (coverage): add cases where yaml.Marshal returns an error // TODO (coverage): add cases where yaml.Unmarshal returns an error ) DescribeTable("EncodePluginConfig should encode the plugin data correctly", func(getPluginCfg func() PluginConfig, expectedCfg func() Cfg) { Expect(c.EncodePluginConfig(key, getPluginCfg())).To(Succeed()) Expect(c).To(Equal(expectedCfg())) }, Entry("for an empty plugin config object", func() PluginConfig { return PluginConfig{} }, func() Cfg { return c1 }), Entry("for a full plugin config object", func() PluginConfig { return pluginCfg }, func() Cfg { return c2 }), // TODO (coverage): add cases where yaml.Marshal returns an error // TODO (coverage): add cases where yaml.Unmarshal returns an error ) }) Context("Persistence", func() { var ( c1, c2 Cfg s1, s1bis, s2 string ) BeforeEach(func() { c1 = Cfg{ Version: Version, Domain: domain, Repository: repo, Name: name, PluginChain: pluginChain, } c2 = Cfg{ Version: Version, Domain: otherDomain, Repository: otherRepo, Name: otherName, PluginChain: otherPluginChain, MultiGroup: true, Resources: []resource.Resource{ { GVK: resource.GVK{ Group: "group", Version: "v1", Kind: "Kind", }, }, { GVK: resource.GVK{ Group: "group", Version: "v1", Kind: "Kind2", }, API: &resource.API{CRDVersion: "v1"}, Controller: true, Webhooks: &resource.Webhooks{WebhookVersion: "v1"}, }, { GVK: resource.GVK{ Group: "group", Version: "v1-beta", Kind: "Kind", }, Plural: "kindes", API: nil, Webhooks: nil, }, { GVK: resource.GVK{ Group: "group2", Version: "v1", Kind: "Kind", }, API: &resource.API{ CRDVersion: "v1", Namespaced: true, }, Controller: true, Webhooks: &resource.Webhooks{ WebhookVersion: "v1", Defaulting: true, Validation: true, Conversion: true, }, }, }, Plugins: pluginConfigs{ "plugin-x": map[string]any{ "data-1": "single plugin datum", }, "plugin-y/v1": map[string]any{ "data-1": "plugin value 1", "data-2": "plugin value 2", "data-3": []string{"plugin value 3", "plugin value 4"}, }, }, } // TODO: include cases with Path when added s1 = `domain: my.domain layout: - go.kubebuilder.io/v2 projectName: ProjectName repo: myrepo version: "3" ` s1bis = `domain: my.domain layout: go.kubebuilder.io/v2 projectName: ProjectName repo: myrepo version: "3" ` s2 = `domain: other.domain layout: - go.kubebuilder.io/v3 multigroup: true plugins: plugin-x: data-1: single plugin datum plugin-y/v1: data-1: plugin value 1 data-2: plugin value 2 data-3: - plugin value 3 - plugin value 4 projectName: OtherProjectName repo: otherrepo resources: - group: group kind: Kind version: v1 - api: crdVersion: v1 controller: true group: group kind: Kind2 version: v1 webhooks: webhookVersion: v1 - group: group kind: Kind plural: kindes version: v1-beta - api: crdVersion: v1 namespaced: true controller: true group: group2 kind: Kind version: v1 webhooks: conversion: true defaulting: true validation: true webhookVersion: v1 version: "3" ` }) DescribeTable("MarshalYAML should succeed", func(getCfg func() Cfg, getContent func() string) { b, err := getCfg().MarshalYAML() Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal(getContent())) }, Entry("for a basic configuration", func() Cfg { return c1 }, func() string { return s1 }), Entry("for a full configuration", func() Cfg { return c2 }, func() string { return s2 }), ) DescribeTable("UnmarshalYAML should succeed", func(getContent func() string, getCfg func() Cfg) { var unmarshalled Cfg Expect(unmarshalled.UnmarshalYAML([]byte(getContent()))).To(Succeed()) c := getCfg() Expect(unmarshalled.Version.Compare(c.Version)).To(Equal(0)) Expect(unmarshalled.Domain).To(Equal(c.Domain)) Expect(unmarshalled.Repository).To(Equal(c.Repository)) Expect(unmarshalled.Name).To(Equal(c.Name)) Expect(unmarshalled.PluginChain).To(Equal(c.PluginChain)) Expect(unmarshalled.MultiGroup).To(Equal(c.MultiGroup)) Expect(unmarshalled.Resources).To(Equal(c.Resources)) Expect(unmarshalled.Plugins).To(HaveLen(len(c.Plugins))) // TODO: fully test Plugins field and not on its length }, Entry("basic", func() string { return s1 }, func() Cfg { return c1 }), Entry("full", func() string { return s2 }, func() Cfg { return c2 }), Entry("string layout", func() string { return s1bis }, func() Cfg { return c1 }), ) DescribeTable("UnmarshalYAML should fail", func(content string) { var c Cfg Expect(c.UnmarshalYAML([]byte(content))).NotTo(Succeed()) }, Entry("for invalid YAML", `invalid: yaml: content badly indented`), ) // Test forward compatibility - unknown fields should be ignored Context("Forward compatibility", func() { It("should ignore unknown fields for forward compatibility", func() { // Old kubebuilder reading PROJECT file with new fields content := `version: "3" domain: example.com repo: github.com/example/project projectName: test-project unknownField: "should be ignored" anotherUnknownField: 123` var c Cfg Expect(c.UnmarshalYAML([]byte(content))).To(Succeed()) Expect(c.Domain).To(Equal("example.com")) Expect(c.Repository).To(Equal("github.com/example/project")) Expect(c.Name).To(Equal("test-project")) }) It("should ignore the new 'controllers' field for backward compatibility", func() { // Simulates old kubebuilder reading PROJECT file from new kubebuilder // that has the "controllers" field in resources content := `version: "3" domain: testproject.org repo: sigs.k8s.io/kubebuilder/testdata/project projectName: test-project resources: - api: crdVersion: v1 namespaced: true controllers: - name: captain - name: captain-backup domain: testproject.org group: crew kind: Captain version: v1` var c Cfg Expect(c.UnmarshalYAML([]byte(content))).To(Succeed()) Expect(c.Domain).To(Equal("testproject.org")) Expect(c.Resources).To(HaveLen(1)) Expect(c.Resources[0].Group).To(Equal("crew")) Expect(c.Resources[0].Kind).To(Equal("Captain")) // The "controllers" field should be silently ignored by older versions }) It("should handle mixed known and unknown fields", func() { content := `version: "3" domain: example.com repo: github.com/example/project projectName: test-project layout: - go.kubebuilder.io/v4 unknownField1: "ignored" multigroup: true unknownField2: nested: "also ignored" resources: - group: apps version: v1 kind: Deployment newField: "ignored in resource"` var c Cfg Expect(c.UnmarshalYAML([]byte(content))).To(Succeed()) Expect(c.Domain).To(Equal("example.com")) Expect(c.MultiGroup).To(BeTrue()) Expect(c.PluginChain).To(Equal(stringSlice{"go.kubebuilder.io/v4"})) Expect(c.Resources).To(HaveLen(1)) }) }) }) }) var _ = Describe("New", func() { It("should return a new config for project configuration 3", func() { Expect(New().GetVersion().Compare(Version)).To(Equal(0)) }) }) ================================================ FILE: pkg/config/version.go ================================================ /* Copyright 2022 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 config import ( "encoding/json" "errors" "fmt" "strconv" "strings" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" ) var ( errNonPositive = errors.New("project version number must be positive") errEmpty = errors.New("project version is empty") ) // Version is a project version containing a non-zero positive integer and a stage value that represents stability. type Version struct { // Number denotes the current version of a plugin. Two different numbers between versions // indicate that they are incompatible. Number int // Stage indicates stability. Stage stage.Stage } // Parse parses version inline, assuming it adheres to format: [1-9][0-9]*(-(alpha|beta))? func (v *Version) Parse(version string) error { if len(version) == 0 { return errEmpty } substrings := strings.SplitN(version, "-", 2) var err error if v.Number, err = strconv.Atoi(substrings[0]); err != nil { // Let's check if the `-` belonged to a negative number if n, errParse := strconv.Atoi(version); errParse == nil && n < 0 { return errNonPositive } return fmt.Errorf("failed to convert version number %q: %w", substrings[0], err) } else if v.Number == 0 { return errNonPositive } if len(substrings) > 1 { if err = v.Stage.Parse(substrings[1]); err != nil { return fmt.Errorf("failed to parse stage: %w", err) } } return nil } // String returns the string representation of v. func (v Version) String() string { stageStr := v.Stage.String() if len(stageStr) == 0 { return fmt.Sprintf("%d", v.Number) } return fmt.Sprintf("%d-%s", v.Number, stageStr) } // Validate ensures that the version number is positive and the stage is one of the valid stages. func (v Version) Validate() error { if v.Number < 1 { return errNonPositive } if err := v.Stage.Validate(); err != nil { return fmt.Errorf("failed to validate stage: %w", err) } return nil } // Compare returns -1 if v < other, 0 if v == other, and 1 if v > other. func (v Version) Compare(other Version) int { if v.Number > other.Number { return 1 } else if v.Number < other.Number { return -1 } return v.Stage.Compare(other.Stage) } // IsStable returns true if v is stable. func (v Version) IsStable() bool { return v.Stage.IsStable() } // MarshalJSON implements json.Marshaller func (v Version) MarshalJSON() ([]byte, error) { if err := v.Validate(); err != nil { return []byte{}, fmt.Errorf("failed to validate version: %w", err) } marshaled, err := json.Marshal(v.String()) if err != nil { return []byte{}, fmt.Errorf("failed to marshal version: %w", err) } return marshaled, nil } // UnmarshalJSON implements json.Unmarshaller func (v *Version) UnmarshalJSON(b []byte) error { var str string if err := json.Unmarshal(b, &str); err != nil { return fmt.Errorf("failed to unmarshal version: %w", err) } return v.Parse(str) } ================================================ FILE: pkg/config/version_test.go ================================================ /* Copyright 2022 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 config import ( "slices" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" ) var _ = Describe("Version", func() { // Parse, String and Validate are tested by MarshalJSON and UnmarshalJSON Context("Compare", func() { var ( versions []Version sortedVersions []Version ) BeforeEach(func() { // Test Compare() by sorting a list. versions = []Version{ {Number: 2, Stage: stage.Alpha}, {Number: 44, Stage: stage.Alpha}, {Number: 1}, {Number: 2, Stage: stage.Beta}, {Number: 4, Stage: stage.Beta}, {Number: 1, Stage: stage.Alpha}, {Number: 4}, {Number: 44, Stage: stage.Alpha}, {Number: 30}, {Number: 4, Stage: stage.Alpha}, } sortedVersions = []Version{ {Number: 1, Stage: stage.Alpha}, {Number: 1}, {Number: 2, Stage: stage.Alpha}, {Number: 2, Stage: stage.Beta}, {Number: 4, Stage: stage.Alpha}, {Number: 4, Stage: stage.Beta}, {Number: 4}, {Number: 30}, {Number: 44, Stage: stage.Alpha}, {Number: 44, Stage: stage.Alpha}, } }) It("sorts a valid list of versions correctly", func() { slices.SortStableFunc(versions, func(a, b Version) int { return a.Compare(b) }) Expect(versions).To(Equal(sortedVersions)) }) }) Context("IsStable", func() { DescribeTable("should return true for stable versions", func(version Version) { Expect(version.IsStable()).To(BeTrue()) }, Entry("for version 1", Version{Number: 1}), Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}), Entry("for version 22", Version{Number: 22}), Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}), ) DescribeTable("should return false for unstable versions", func(version Version) { Expect(version.IsStable()).To(BeFalse()) }, Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}), Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}), Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}), Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), ) }) Context("MarshalJSON", func() { DescribeTable("should be marshalled appropriately", func(version Version, str string) { b, err := version.MarshalJSON() Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal(str)) }, Entry("for version 1", Version{Number: 1}, `"1"`), Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}, `"1"`), Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}, `"1-alpha"`), Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}, `"1-beta"`), Entry("for version 22", Version{Number: 22}, `"22"`), Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}, `"22"`), Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}, `"22-alpha"`), Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}, `"22-beta"`), ) DescribeTable("should fail to be marshalled", func(version Version) { _, err := version.MarshalJSON() Expect(err).To(HaveOccurred()) }, Entry("for version 0", Version{Number: 0}), Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}), Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}), Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}), Entry("for version 0 (implicit)", Version{}), Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}), Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}), Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}), Entry("for version -1", Version{Number: -1}), Entry("for version -1 (stable)", Version{Number: -1, Stage: stage.Stable}), Entry("for version -1 (alpha)", Version{Number: -1, Stage: stage.Alpha}), Entry("for version -1 (beta)", Version{Number: -1, Stage: stage.Beta}), Entry("for invalid stage", Version{Stage: stage.Stage(34)}), ) }) Context("UnmarshalJSON", func() { DescribeTable("should be unmarshalled appropriately", func(str string, number int, s stage.Stage) { var v Version err := v.UnmarshalJSON([]byte(str)) Expect(err).NotTo(HaveOccurred()) Expect(v.Number).To(Equal(number)) Expect(v.Stage).To(Equal(s)) }, Entry("for version string `1`", `"1"`, 1, stage.Stable), Entry("for version string `1-alpha`", `"1-alpha"`, 1, stage.Alpha), Entry("for version string `1-beta`", `"1-beta"`, 1, stage.Beta), Entry("for version string `22`", `"22"`, 22, stage.Stable), Entry("for version string `22-alpha`", `"22-alpha"`, 22, stage.Alpha), Entry("for version string `22-beta`", `"22-beta"`, 22, stage.Beta), ) DescribeTable("should fail to be unmarshalled", func(str string) { var v Version err := v.UnmarshalJSON([]byte(str)) Expect(err).To(HaveOccurred()) }, Entry("for empty version string", ``), Entry("for version string ``", `""`), Entry("for version string `0`", `"0"`), Entry("for version string `0-alpha`", `"0-alpha"`), Entry("for version string `0-beta`", `"0-beta"`), Entry("for version string `-1`", `"-1"`), Entry("for version string `-1-alpha`", `"-1-alpha"`), Entry("for version string `-1-beta`", `"-1-beta"`), Entry("for version string `v1`", `"v1"`), Entry("for version string `v1-alpha`", `"v1-alpha"`), Entry("for version string `v1-beta`", `"v1-beta"`), Entry("for version string `1.0`", `"1.0"`), Entry("for version string `v1.0`", `"v1.0"`), Entry("for version string `v1.0-alpha`", `"v1.0-alpha"`), Entry("for version string `1.0.0`", `"1.0.0"`), Entry("for version string `1-a`", `"1-a"`), ) }) }) ================================================ FILE: pkg/machinery/errors.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package machinery import ( "fmt" ) // This file contains the errors returned by the scaffolding machinery // They are exported to be able to check which kind of error was returned // ValidateError is a wrapper error that will be used for errors returned by RequiresValidation.Validate type ValidateError struct { error } // Unwrap implements Wrapper interface func (e ValidateError) Unwrap() error { return e.error } // SetTemplateDefaultsError is a wrapper error that will be used for errors returned by Template.SetTemplateDefaults type SetTemplateDefaultsError struct { error } // Unwrap implements Wrapper interface func (e SetTemplateDefaultsError) Unwrap() error { return e.error } // ExistsFileError is a wrapper error that will be used for errors when checking for a file existence type ExistsFileError struct { error } // Unwrap implements Wrapper interface func (e ExistsFileError) Unwrap() error { return e.error } // OpenFileError is a wrapper error that will be used for errors when opening a file type OpenFileError struct { error } // Unwrap implements Wrapper interface func (e OpenFileError) Unwrap() error { return e.error } // CreateDirectoryError is a wrapper error that will be used for errors when creating a directory type CreateDirectoryError struct { error } // Unwrap implements Wrapper interface func (e CreateDirectoryError) Unwrap() error { return e.error } // CreateFileError is a wrapper error that will be used for errors when creating a file type CreateFileError struct { error } // Unwrap implements Wrapper interface func (e CreateFileError) Unwrap() error { return e.error } // ReadFileError is a wrapper error that will be used for errors when reading a file type ReadFileError struct { error } // Unwrap implements Wrapper interface func (e ReadFileError) Unwrap() error { return e.error } // WriteFileError is a wrapper error that will be used for errors when writing a file type WriteFileError struct { error } // Unwrap implements Wrapper interface func (e WriteFileError) Unwrap() error { return e.error } // CloseFileError is a wrapper error that will be used for errors when closing a file type CloseFileError struct { error } // Unwrap implements Wrapper interface func (e CloseFileError) Unwrap() error { return e.error } // ModelAlreadyExistsError is returned if the file is expected not to exist but a previous model does type ModelAlreadyExistsError struct { path string } // Error implements error interface func (e ModelAlreadyExistsError) Error() string { return fmt.Sprintf("failed to create %s: model already exists", e.path) } // UnknownIfExistsActionError is returned if the if-exists-action is unknown type UnknownIfExistsActionError struct { path string ifExistsAction IfExistsAction } // Error implements error interface func (e UnknownIfExistsActionError) Error() string { return fmt.Sprintf("unknown behavior if file exists (%d) for %s", e.ifExistsAction, e.path) } // FileAlreadyExistsError is returned if the file is expected not to exist but it does type FileAlreadyExistsError struct { path string } // Error implements error interface func (e FileAlreadyExistsError) Error() string { return fmt.Sprintf("failed to create %s: file already exists", e.path) } ================================================ FILE: pkg/machinery/errors_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package machinery import ( "errors" "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Errors", func() { var ( path string testErr error ) BeforeEach(func() { path = filepath.Join("path", "to", "file") testErr = errors.New("test error") }) DescribeTable("should contain the wrapped error", func(getErr func() error) { err := getErr() Expect(err).To(HaveOccurred()) Expect(errors.Is(err, testErr)).To(BeTrue()) }, Entry("for validate errors", func() error { return ValidateError{testErr} }), Entry("for set template defaults errors", func() error { return SetTemplateDefaultsError{testErr} }), Entry("for file existence errors", func() error { return ExistsFileError{testErr} }), Entry("for file opening errors", func() error { return OpenFileError{testErr} }), Entry("for directory creation errors", func() error { return CreateDirectoryError{testErr} }), Entry("for file creation errors", func() error { return CreateFileError{testErr} }), Entry("for file reading errors", func() error { return ReadFileError{testErr} }), Entry("for file writing errors", func() error { return WriteFileError{testErr} }), Entry("for file closing errors", func() error { return CloseFileError{testErr} }), ) // NOTE: the following test increases coverage It("should print a descriptive error message", func() { Expect(ModelAlreadyExistsError{path}.Error()).To(ContainSubstring("model already exists")) Expect(UnknownIfExistsActionError{path, -1}.Error()).To(ContainSubstring("unknown behavior if file exists")) Expect(FileAlreadyExistsError{path}.Error()).To(ContainSubstring("file already exists")) }) }) ================================================ FILE: pkg/machinery/file.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package machinery // IfExistsAction determines what to do if the scaffold file already exists type IfExistsAction int const ( // SkipFile skips the file and moves to the next one SkipFile IfExistsAction = iota // Error returns an error and stops processing Error // OverwriteFile truncates and overwrites the existing file OverwriteFile ) // IfNotExistsAction determines what to do if a file to be updated does not exist type IfNotExistsAction int const ( // ErrorIfNotExist returns an error and stops processing (default behavior) ErrorIfNotExist IfNotExistsAction = iota // IgnoreFile skips the file and logs a message if it does not exist IgnoreFile ) // File describes a file that will be written type File struct { // Path is the file to write Path string // Contents is the generated output Contents string // IfExistsAction determines what to do if the file exists IfExistsAction IfExistsAction // IfNotExistsAction determines what to do if the file is missing (optional updates only) IfNotExistsAction IfNotExistsAction } ================================================ FILE: pkg/machinery/filesystem.go ================================================ /* Copyright 2022 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 machinery import ( "github.com/spf13/afero" ) // Filesystem abstracts the underlying disk for scaffolding type Filesystem struct { FS afero.Fs } ================================================ FILE: pkg/machinery/funcmap.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package machinery import ( "fmt" "hash/fnv" "strings" "text/template" "golang.org/x/text/cases" ) // DefaultFuncMap returns the default template.FuncMap for rendering the template. func DefaultFuncMap() template.FuncMap { return template.FuncMap{ "title": cases.Title, "lower": strings.ToLower, "upper": strings.ToUpper, "isEmptyStr": isEmptyString, "hashFNV": hashFNV, } } // isEmptyString returns whether the string is empty func isEmptyString(s string) bool { return s == "" } // hashFNV will generate a random string useful for generating a unique string func hashFNV(s string) string { hasher := fnv.New32a() // Hash.Write never returns an error _, _ = hasher.Write([]byte(s)) return fmt.Sprintf("%x", hasher.Sum(nil)) } ================================================ FILE: pkg/machinery/funcmap_test.go ================================================ /* Copyright 2022 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 machinery import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("funcmap functions", func() { Context("isEmptyString", func() { It("should return true for empty strings", func() { Expect(isEmptyString("")).To(BeTrue()) }) DescribeTable("should return false for any other string", func(str string) { Expect(isEmptyString(str)).To(BeFalse()) }, Entry(`for "a"`, "a"), Entry(`for "1"`, "1"), Entry(`for "-"`, "-"), Entry(`for "."`, "."), ) }) Context("hashFNV", func() { It("should hash the input", func() { Expect(hashFNV("test")).To(Equal("afd071e5")) }) }) }) ================================================ FILE: pkg/machinery/injector.go ================================================ /* Copyright 2022 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 machinery import ( "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) // injector is used to inject certain fields to file templates. type injector struct { // config stores the project configuration. config config.Config // boilerplate is the copyright comment added at the top of scaffolded files. boilerplate string // resource contains the information of the API that is being scaffolded. resource *resource.Resource } // injectInto injects fields from the universe into the builder func (i injector) injectInto(builder Builder) { // Inject project configuration if i.config != nil { if builderWithDomain, hasDomain := builder.(HasDomain); hasDomain { builderWithDomain.InjectDomain(i.config.GetDomain()) } if builderWithRepository, hasRepository := builder.(HasRepository); hasRepository { builderWithRepository.InjectRepository(i.config.GetRepository()) } if builderWithProjectName, hasProjectName := builder.(HasProjectName); hasProjectName { builderWithProjectName.InjectProjectName(i.config.GetProjectName()) } if builderWithMultiGroup, hasMultiGroup := builder.(HasMultiGroup); hasMultiGroup { builderWithMultiGroup.InjectMultiGroup(i.config.IsMultiGroup()) } if builderWithNamespaced, hasNamespaced := builder.(HasNamespaced); hasNamespaced { builderWithNamespaced.InjectNamespaced(i.config.IsNamespaced()) } } // Inject boilerplate if builderWithBoilerplate, hasBoilerplate := builder.(HasBoilerplate); hasBoilerplate { builderWithBoilerplate.InjectBoilerplate(i.boilerplate) } // Inject resource if i.resource != nil { if builderWithResource, hasResource := builder.(HasResource); hasResource { builderWithResource.InjectResource(i.resource) } } } ================================================ FILE: pkg/machinery/injector_test.go ================================================ /* Copyright 2022 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 machinery import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) type templateBase struct { path string ifExistsAction IfExistsAction } func (t templateBase) GetPath() string { return t.path } func (t templateBase) GetIfExistsAction() IfExistsAction { return t.ifExistsAction } type templateWithDomain struct { templateBase domain string } func (t *templateWithDomain) InjectDomain(domain string) { t.domain = domain } type templateWithRepository struct { templateBase repository string } func (t *templateWithRepository) InjectRepository(repository string) { t.repository = repository } type templateWithProjectName struct { templateBase projectName string } func (t *templateWithProjectName) InjectProjectName(projectName string) { t.projectName = projectName } type templateWithMultiGroup struct { templateBase multiGroup bool } func (t *templateWithMultiGroup) InjectMultiGroup(multiGroup bool) { t.multiGroup = multiGroup } type templateWithBoilerplate struct { templateBase boilerplate string } func (t *templateWithBoilerplate) InjectBoilerplate(boilerplate string) { t.boilerplate = boilerplate } type templateWithResource struct { templateBase resource *resource.Resource } func (t *templateWithResource) InjectResource(res *resource.Resource) { t.resource = res } var _ = Describe("injector", func() { var tmp templateBase BeforeEach(func() { tmp = templateBase{ path: "my/path/to/file", ifExistsAction: Error, } }) Context("injectInto", func() { Context("Config", func() { var c config.Config BeforeEach(func() { c = cfgv3.New() }) Context("Domain", func() { var template *templateWithDomain BeforeEach(func() { template = &templateWithDomain{templateBase: tmp} }) It("should not inject anything if the config is nil", func() { injector{}.injectInto(template) Expect(template.domain).To(Equal("")) }) It("should not inject anything if the config doesn't have a domain set", func() { injector{config: c}.injectInto(template) Expect(template.domain).To(Equal("")) }) It("should inject if the config has a domain set", func() { const domain = "my.domain" Expect(c.SetDomain(domain)).To(Succeed()) injector{config: c}.injectInto(template) Expect(template.domain).To(Equal(domain)) }) }) Context("Repository", func() { var template *templateWithRepository BeforeEach(func() { template = &templateWithRepository{templateBase: tmp} }) It("should not inject anything if the config is nil", func() { injector{}.injectInto(template) Expect(template.repository).To(Equal("")) }) It("should not inject anything if the config doesn't have a repository set", func() { injector{config: c}.injectInto(template) Expect(template.repository).To(Equal("")) }) It("should inject if the config has a repository set", func() { const repo = "test" Expect(c.SetRepository(repo)).To(Succeed()) injector{config: c}.injectInto(template) Expect(template.repository).To(Equal(repo)) }) }) Context("Project name", func() { var template *templateWithProjectName BeforeEach(func() { template = &templateWithProjectName{templateBase: tmp} }) It("should not inject anything if the config is nil", func() { injector{}.injectInto(template) Expect(template.projectName).To(Equal("")) }) It("should not inject anything if the config doesn't have a project name set", func() { injector{config: c}.injectInto(template) Expect(template.projectName).To(Equal("")) }) It("should inject if the config has a project name set", func() { const projectName = "my project" Expect(c.SetProjectName(projectName)).To(Succeed()) injector{config: c}.injectInto(template) Expect(template.projectName).To(Equal(projectName)) }) }) Context("Multi-group", func() { var template *templateWithMultiGroup BeforeEach(func() { template = &templateWithMultiGroup{templateBase: tmp} }) It("should not inject anything if the config is nil", func() { injector{}.injectInto(template) Expect(template.multiGroup).To(BeFalse()) }) It("should not set the flag if the config doesn't have the multi-group flag set", func() { injector{config: c}.injectInto(template) Expect(template.multiGroup).To(BeFalse()) }) It("should set the flag if the config has the multi-group flag set", func() { Expect(c.SetMultiGroup()).To(Succeed()) injector{config: c}.injectInto(template) Expect(template.multiGroup).To(BeTrue()) }) }) }) Context("Boilerplate", func() { var template *templateWithBoilerplate BeforeEach(func() { template = &templateWithBoilerplate{templateBase: tmp} }) It("should not inject anything if no boilerplate was set", func() { injector{}.injectInto(template) Expect(template.boilerplate).To(Equal("")) }) It("should inject if the a boilerplate was set", func() { const boilerplate = `Copyright "The Kubernetes Authors"` injector{boilerplate: boilerplate}.injectInto(template) Expect(template.boilerplate).To(Equal(boilerplate)) }) }) Context("Resource", func() { var template *templateWithResource BeforeEach(func() { template = &templateWithResource{templateBase: tmp} }) It("should not inject anything if the resource is nil", func() { injector{}.injectInto(template) Expect(template.resource).To(BeNil()) }) It("should inject if the config has a domain set", func() { res := &resource.Resource{ GVK: resource.GVK{ Group: "group", Domain: "my.domain", Version: "v1", Kind: "Kind", }, } injector{resource: res}.injectInto(template) Expect(template.resource).To(Equal(res)) }) }) }) }) ================================================ FILE: pkg/machinery/interfaces.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 machinery import ( "text/template" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) // Builder defines the basic methods that any file builder must implement type Builder interface { // GetPath returns the path to the file location GetPath() string // GetIfExistsAction returns the behavior when creating a file that already exists GetIfExistsAction() IfExistsAction } // RequiresValidation is a file builder that requires validation type RequiresValidation interface { Builder // Validate returns true if the template has valid values Validate() error } // Template is file builder based on a file template type Template interface { Builder // GetBody returns the template body GetBody() string // SetTemplateDefaults sets the default values for templates SetTemplateDefaults() error // SetDelim sets an action delimiters to replace default delimiters: {{ }} SetDelim(left, right string) // GetDelim returns the alternative delimiters GetDelim() (string, string) } // Inserter is a file builder that inserts code fragments in marked positions type Inserter interface { Builder // GetMarkers returns the different markers where code fragments will be inserted GetMarkers() []Marker // GetCodeFragments returns a map that binds markers to code fragments GetCodeFragments() CodeFragmentsMap } // HasIfNotExistsAction allows a template to define an action if the file is missing type HasIfNotExistsAction interface { GetIfNotExistsAction() IfNotExistsAction } // HasDomain allows the domain to be used on a template type HasDomain interface { // InjectDomain sets the template domain InjectDomain(string) } // HasRepository allows the repository to be used on a template type HasRepository interface { // InjectRepository sets the template repository InjectRepository(string) } // HasProjectName allows a project name to be used on a template. type HasProjectName interface { // InjectProjectName sets the template project name. InjectProjectName(string) } // HasMultiGroup allows the multi-group flag to be used on a template type HasMultiGroup interface { // InjectMultiGroup sets the template multi-group flag InjectMultiGroup(bool) } // HasNamespaced allows the namespaced flag to be used on a template type HasNamespaced interface { // InjectNamespaced sets the template namespaced flag InjectNamespaced(bool) } // HasBoilerplate allows a boilerplate to be used on a template type HasBoilerplate interface { // InjectBoilerplate sets the template boilerplate InjectBoilerplate(string) } // HasResource allows a resource to be used on a template type HasResource interface { // InjectResource sets the template resource InjectResource(*resource.Resource) } // UseCustomFuncMap allows a template to use a custom template.FuncMap instead of the default FuncMap. type UseCustomFuncMap interface { // GetFuncMap returns a custom FuncMap. GetFuncMap() template.FuncMap } ================================================ FILE: pkg/machinery/machinery_suite_test.go ================================================ /* Copyright 2022 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 machinery import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestMachinery(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Machinery suite") } ================================================ FILE: pkg/machinery/marker.go ================================================ /* Copyright 2019 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 machinery import ( "fmt" "path/filepath" "strings" ) const kbPrefix = "+kubebuilder:scaffold:" var commentsByExt = map[string]string{ ".go": "//", ".yaml": "#", ".yml": "#", // When adding additional file extensions, update also the NewMarkerFor documentation and error } // Marker represents a machine-readable comment that will be used for scaffolding purposes type Marker struct { prefix string comment string value string } // NewMarkerFor creates a new marker customized for the specific file. The created marker // is prefixed with `+kubebuilder:scaffold:` the default prefix for kubebuilder. // Supported file extensions: .go, .yaml, .yml. func NewMarkerFor(path string, value string) Marker { return NewMarkerWithPrefixFor(kbPrefix, path, value) } // NewMarkerWithPrefixFor creates a new custom prefixed marker customized for the specific file // Supported file extensions: .go, .yaml, .yml func NewMarkerWithPrefixFor(prefix string, path string, value string) Marker { ext := filepath.Ext(path) if comment, found := commentsByExt[ext]; found { return Marker{ prefix: markerPrefix(prefix), comment: comment, value: value, } } extensions := make([]string, 0, len(commentsByExt)) for extension := range commentsByExt { extensions = append(extensions, fmt.Sprintf("%q", extension)) } panic(fmt.Errorf("unknown file extension: '%s', expected one of: %s", ext, strings.Join(extensions, ", "))) } // String implements Stringer func (m Marker) String() string { return m.comment + " " + m.prefix + m.value } // EqualsLine compares a marker with a string representation to check if they are the same marker func (m Marker) EqualsLine(line string) bool { line = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), m.comment)) return line == m.prefix+m.value } // CodeFragments represents a set of code fragments // A code fragment is a piece of code provided as a Go string, it may have multiple lines type CodeFragments []string // CodeFragmentsMap binds Markers and CodeFragments together type CodeFragmentsMap map[Marker]CodeFragments func markerPrefix(prefix string) string { trimmed := strings.TrimSpace(prefix) var builder strings.Builder if !strings.HasPrefix(trimmed, "+") { builder.WriteString("+") } builder.WriteString(trimmed) if !strings.HasSuffix(trimmed, ":") { builder.WriteString(":") } return builder.String() } ================================================ FILE: pkg/machinery/marker_test.go ================================================ /* Copyright 2022 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 machinery import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("NewMarkerFor", func() { DescribeTable("should create valid markers for known extensions", func(path, comment string) { Expect(NewMarkerFor(path, "").comment).To(Equal(comment)) }, Entry("for go files", "file.go", "//"), Entry("for yaml files", "file.yaml", "#"), Entry("for yaml files (short version)", "file.yml", "#"), ) It("should panic for unknown extensions", func() { // testing panics require to use a function with no arguments Expect(func() { NewMarkerFor("file.unkownext", "") }).To(Panic()) }) }) var _ = Describe("Marker", func() { Context("String", func() { DescribeTable("should return the right string representation", func(marker Marker, str string) { Expect(marker.String()).To(Equal(str)) }, Entry("for go files", Marker{prefix: kbPrefix, comment: "//", value: "test"}, "// +kubebuilder:scaffold:test"), Entry("for yaml files", Marker{prefix: kbPrefix, comment: "#", value: "test"}, "# +kubebuilder:scaffold:test"), ) }) }) var _ = Describe("NewMarkerFor", func() { Context("String", func() { DescribeTable("should return the right string representation", func(marker Marker, str string) { Expect(marker.String()).To(Equal(str)) }, Entry("for yaml files", NewMarkerFor("test.yaml", "test"), "# +kubebuilder:scaffold:test"), ) }) }) var _ = Describe("NewMarkerForImports", func() { Context("String", func() { DescribeTable("should return the correct string representation for import markers", func(marker Marker, str string) { Expect(marker.String()).To(Equal(str)) }, Entry("for go import marker", NewMarkerFor("test.go", "import \"my/package\""), "// +kubebuilder:scaffold:import \"my/package\""), Entry("for go import marker with alias", NewMarkerFor("test.go", "import alias \"my/package\""), "// +kubebuilder:scaffold:import alias \"my/package\""), Entry("for multiline go import marker", NewMarkerFor("test.go", "import (\n\"my/package\"\n)"), "// +kubebuilder:scaffold:import (\n\"my/package\"\n)"), Entry("for multiline go import marker with alias", NewMarkerFor("test.go", "import (\nalias \"my/package\"\n)"), "// +kubebuilder:scaffold:import (\nalias \"my/package\"\n)"), ) }) It("should detect import in Go file", func() { line := "// +kubebuilder:scaffold:import \"my/package\"" marker := NewMarkerFor("test.go", "import \"my/package\"") Expect(marker.EqualsLine(line)).To(BeTrue()) }) It("should detect import with alias in Go file", func() { line := "// +kubebuilder:scaffold:import alias \"my/package\"" marker := NewMarkerFor("test.go", "import alias \"my/package\"") Expect(marker.EqualsLine(line)).To(BeTrue()) }) It("should detect multiline import in Go file", func() { line := "// +kubebuilder:scaffold:import (\n\"my/package\"\n)" marker := NewMarkerFor("test.go", "import (\n\"my/package\"\n)") Expect(marker.EqualsLine(line)).To(BeTrue()) }) It("should detect multiline import with alias in Go file", func() { line := "// +kubebuilder:scaffold:import (\nalias \"my/package\"\n)" marker := NewMarkerFor("test.go", "import (\nalias \"my/package\"\n)") Expect(marker.EqualsLine(line)).To(BeTrue()) }) }) var _ = Describe("NewMarkerForImports with different formatting", func() { Context("String", func() { DescribeTable("should handle variations in spacing and formatting for import markers", func(marker Marker, str string) { Expect(marker.String()).To(Equal(str)) }, Entry("go import marker with extra spaces", NewMarkerFor("test.go", "import \"my/package\""), "// +kubebuilder:scaffold:import \"my/package\""), Entry("go import marker with spaces around alias", NewMarkerFor("test.go", "import alias \"my/package\""), "// +kubebuilder:scaffold:import alias \"my/package\""), Entry("go import marker with newline", NewMarkerFor("test.go", "import \n\"my/package\""), "// +kubebuilder:scaffold:import \n\"my/package\""), ) }) It("should detect import with spaces in Go file", func() { line := "// +kubebuilder:scaffold:import \"my/package\"" marker := NewMarkerFor("test.go", "import \"my/package\"") Expect(marker.EqualsLine(line)).To(BeTrue()) }) It("should detect import with alias and spaces in Go file", func() { line := "// +kubebuilder:scaffold:import alias \"my/package\"" marker := NewMarkerFor("test.go", "import alias \"my/package\"") Expect(marker.EqualsLine(line)).To(BeTrue()) }) }) var _ = Describe("NewMarkerWithPrefixFor", func() { Context("String", func() { DescribeTable("should return the right string representation", func(marker Marker, str string) { Expect(marker.String()).To(Equal(str)) }, Entry("for yaml files", NewMarkerWithPrefixFor("custom:scaffold", "test.yaml", "test"), "# +custom:scaffold:test"), Entry("for yaml files", NewMarkerWithPrefixFor("+custom:scaffold", "test.yaml", "test"), "# +custom:scaffold:test"), Entry("for yaml files", NewMarkerWithPrefixFor("custom:scaffold:", "test.yaml", "test"), "# +custom:scaffold:test"), Entry("for yaml files", NewMarkerWithPrefixFor("+custom:scaffold:", "test.yaml", "test"), "# +custom:scaffold:test"), Entry("for yaml files", NewMarkerWithPrefixFor(" +custom:scaffold: ", "test.yaml", "test"), "# +custom:scaffold:test"), Entry("for go files", NewMarkerWithPrefixFor("custom:scaffold", "test.go", "test"), "// +custom:scaffold:test"), Entry("for go files", NewMarkerWithPrefixFor("+custom:scaffold", "test.go", "test"), "// +custom:scaffold:test"), Entry("for go files", NewMarkerWithPrefixFor("custom:scaffold:", "test.go", "test"), "// +custom:scaffold:test"), Entry("for go files", NewMarkerWithPrefixFor("+custom:scaffold:", "test.go", "test"), "// +custom:scaffold:test"), Entry("for go files", NewMarkerWithPrefixFor(" +custom:scaffold: ", "test.go", "test"), "// +custom:scaffold:test"), ) }) }) var _ = Describe("NewMarkerFor with unsupported extensions", func() { It("should panic for unsupported extensions", func() { Expect(func() { NewMarkerFor("file.txt", "test") }).To(Panic()) Expect(func() { NewMarkerFor("file.md", "test") }).To(Panic()) }) }) ================================================ FILE: pkg/machinery/mixins.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 machinery import ( "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) // PathMixin provides file builders with a path field type PathMixin struct { // Path is the of the file Path string } // GetPath implements Builder func (t *PathMixin) GetPath() string { return t.Path } // IfExistsActionMixin provides file builders with a if-exists-action field type IfExistsActionMixin struct { // IfExistsAction determines what to do if the file exists IfExistsAction IfExistsAction } // GetIfExistsAction implements Builder func (t *IfExistsActionMixin) GetIfExistsAction() IfExistsAction { return t.IfExistsAction } // TemplateMixin is the mixin that should be embedded in Template builders type TemplateMixin struct { PathMixin IfExistsActionMixin // TemplateBody is the template body to execute TemplateBody string parseDelimLeft string parseDelimRight string } // GetBody implements Template func (t *TemplateMixin) GetBody() string { return t.TemplateBody } // SetDelim implements Template func (t *TemplateMixin) SetDelim(left, right string) { t.parseDelimLeft = left t.parseDelimRight = right } // GetDelim implements Template func (t *TemplateMixin) GetDelim() (string, string) { return t.parseDelimLeft, t.parseDelimRight } // InserterMixin is the mixin that should be embedded in Inserter builders type InserterMixin struct { PathMixin } // GetIfExistsAction implements Builder func (t *InserterMixin) GetIfExistsAction() IfExistsAction { // Inserter builders always need to overwrite previous files return OverwriteFile } // DomainMixin provides templates with a injectable domain field type DomainMixin struct { // Domain is the domain for the APIs Domain string } // InjectDomain implements HasDomain func (m *DomainMixin) InjectDomain(domain string) { if m.Domain == "" { m.Domain = domain } } // RepositoryMixin provides templates with a injectable repository field type RepositoryMixin struct { // Repo is the go project package path Repo string } // InjectRepository implements HasRepository func (m *RepositoryMixin) InjectRepository(repository string) { if m.Repo == "" { m.Repo = repository } } // ProjectNameMixin provides templates with an injectable project name field. type ProjectNameMixin struct { ProjectName string } // InjectProjectName implements HasProjectName. func (m *ProjectNameMixin) InjectProjectName(projectName string) { if m.ProjectName == "" { m.ProjectName = projectName } } // MultiGroupMixin provides templates with a injectable multi-group flag field type MultiGroupMixin struct { // MultiGroup is the multi-group flag MultiGroup bool } // InjectMultiGroup implements HasMultiGroup func (m *MultiGroupMixin) InjectMultiGroup(flag bool) { m.MultiGroup = flag } // NamespacedMixin provides templates with a injectable namespaced flag field type NamespacedMixin struct { // Namespaced is the namespaced flag Namespaced bool } // InjectNamespaced implements HasNamespaced func (m *NamespacedMixin) InjectNamespaced(flag bool) { m.Namespaced = flag } // BoilerplateMixin provides templates with a injectable boilerplate field type BoilerplateMixin struct { // Boilerplate is the contents of a Boilerplate go header file Boilerplate string } // InjectBoilerplate implements HasBoilerplate func (m *BoilerplateMixin) InjectBoilerplate(boilerplate string) { if m.Boilerplate == "" { m.Boilerplate = boilerplate } } // ResourceMixin provides templates with a injectable resource field type ResourceMixin struct { Resource *resource.Resource } // InjectResource implements HasResource func (m *ResourceMixin) InjectResource(res *resource.Resource) { if m.Resource == nil { m.Resource = res } } // IfNotExistsActionMixin provides file builders with an if-not-exists-action field type IfNotExistsActionMixin struct { // IfNotExistsAction determines what to do if the file does not exist IfNotExistsAction IfNotExistsAction } // GetIfNotExistsAction implements Inserter func (m *IfNotExistsActionMixin) GetIfNotExistsAction() IfNotExistsAction { return m.IfNotExistsAction } ================================================ FILE: pkg/machinery/mixins_delim_test.go ================================================ /* Copyright 2025 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 machinery import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) var _ = Describe("TemplateMixin Delimiters", func() { var tmp TemplateMixin BeforeEach(func() { tmp = TemplateMixin{} }) Context("SetDelim and GetDelim", func() { It("should set and get custom delimiters", func() { tmp.SetDelim("[[", "]]") left, right := tmp.GetDelim() Expect(left).To(Equal("[[")) Expect(right).To(Equal("]]")) }) It("should return empty strings when delimiters are not set", func() { left, right := tmp.GetDelim() Expect(left).To(Equal("")) Expect(right).To(Equal("")) }) It("should allow setting delimiters multiple times", func() { tmp.SetDelim("[[", "]]") left, right := tmp.GetDelim() Expect(left).To(Equal("[[")) Expect(right).To(Equal("]]")) tmp.SetDelim("<%", "%>") left, right = tmp.GetDelim() Expect(left).To(Equal("<%")) Expect(right).To(Equal("%>")) }) }) }) var _ = Describe("Mixins injection behaviors", func() { Context("DomainMixin", func() { It("should not overwrite existing domain", func() { tmp := DomainMixin{Domain: "existing.domain"} tmp.InjectDomain("new.domain") Expect(tmp.Domain).To(Equal("existing.domain")) }) It("should inject domain when empty", func() { tmp := DomainMixin{} tmp.InjectDomain("new.domain") Expect(tmp.Domain).To(Equal("new.domain")) }) }) Context("RepositoryMixin", func() { It("should not overwrite existing repository", func() { tmp := RepositoryMixin{Repo: "existing.repo"} tmp.InjectRepository("new.repo") Expect(tmp.Repo).To(Equal("existing.repo")) }) It("should inject repository when empty", func() { tmp := RepositoryMixin{} tmp.InjectRepository("new.repo") Expect(tmp.Repo).To(Equal("new.repo")) }) }) Context("ProjectNameMixin", func() { It("should not overwrite existing project name", func() { tmp := ProjectNameMixin{ProjectName: "existing"} tmp.InjectProjectName("new") Expect(tmp.ProjectName).To(Equal("existing")) }) It("should inject project name when empty", func() { tmp := ProjectNameMixin{} tmp.InjectProjectName("new") Expect(tmp.ProjectName).To(Equal("new")) }) }) Context("BoilerplateMixin", func() { It("should not overwrite existing boilerplate", func() { tmp := BoilerplateMixin{Boilerplate: "existing"} tmp.InjectBoilerplate("new") Expect(tmp.Boilerplate).To(Equal("existing")) }) It("should inject boilerplate when empty", func() { tmp := BoilerplateMixin{} tmp.InjectBoilerplate("new") Expect(tmp.Boilerplate).To(Equal("new")) }) }) Context("ResourceMixin", func() { It("should not overwrite existing resource", func() { existing := &resource.Resource{GVK: resource.GVK{Group: "existing"}} tmp := ResourceMixin{Resource: existing} tmp.InjectResource(&resource.Resource{GVK: resource.GVK{Group: "new"}}) Expect(tmp.Resource.Group).To(Equal("existing")) }) It("should inject resource when nil", func() { tmp := ResourceMixin{} res := &resource.Resource{GVK: resource.GVK{Group: "new"}} tmp.InjectResource(res) Expect(tmp.Resource.Group).To(Equal("new")) }) }) }) var _ = Describe("IfNotExistsActionMixin", func() { Context("GetIfNotExistsAction", func() { It("should return the configured action", func() { tmp := IfNotExistsActionMixin{IfNotExistsAction: IgnoreFile} Expect(tmp.GetIfNotExistsAction()).To(Equal(IgnoreFile)) }) It("should return zero value when not set", func() { tmp := IfNotExistsActionMixin{} Expect(tmp.GetIfNotExistsAction()).To(Equal(IfNotExistsAction(0))) }) }) }) ================================================ FILE: pkg/machinery/mixins_test.go ================================================ /* Copyright 2022 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 machinery import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) type mockTemplate struct { TemplateMixin DomainMixin RepositoryMixin ProjectNameMixin MultiGroupMixin BoilerplateMixin ResourceMixin } type mockInserter struct { // InserterMixin requires a different type because it collides with TemplateMixin InserterMixin } var _ = Describe("TemplateMixin", func() { const ( path = "path/to/file.go" ifExistsAction = SkipFile body = "content" ) var tmp mockTemplate BeforeEach(func() { tmp = mockTemplate{ TemplateMixin: TemplateMixin{ PathMixin: PathMixin{path}, IfExistsActionMixin: IfExistsActionMixin{ifExistsAction}, TemplateBody: body, }, } }) Context("GetPath", func() { It("should return the path", func() { Expect(tmp.GetPath()).To(Equal(path)) }) }) Context("GetIfExistsAction", func() { It("should return the if-exists action", func() { Expect(tmp.GetIfExistsAction()).To(Equal(ifExistsAction)) }) }) Context("GetBody", func() { It("should return the body", func() { Expect(tmp.GetBody()).To(Equal(body)) }) }) }) var _ = Describe("InserterMixin", func() { const path = "path/to/file.go" var tmp mockInserter BeforeEach(func() { tmp = mockInserter{ InserterMixin: InserterMixin{ PathMixin: PathMixin{path}, }, } }) Context("GetPath", func() { It("should return the path", func() { Expect(tmp.GetPath()).To(Equal(path)) }) }) Context("GetIfExistsAction", func() { It("should return overwrite file always", func() { Expect(tmp.GetIfExistsAction()).To(Equal(OverwriteFile)) }) }) }) var _ = Describe("DomainMixin", func() { const domain = "my.domain" var tmp mockTemplate BeforeEach(func() { tmp = mockTemplate{} }) Context("InjectDomain", func() { It("should inject the provided domain", func() { tmp.InjectDomain(domain) Expect(tmp.Domain).To(Equal(domain)) }) }) }) var _ = Describe("RepositoryMixin", func() { const repo = "test" var tmp mockTemplate BeforeEach(func() { tmp = mockTemplate{} }) Context("InjectRepository", func() { It("should inject the provided repository", func() { tmp.InjectRepository(repo) Expect(tmp.Repo).To(Equal(repo)) }) }) }) var _ = Describe("ProjectNameMixin", func() { const name = "my project" var tmp mockTemplate BeforeEach(func() { tmp = mockTemplate{} }) Context("InjectProjectName", func() { It("should inject the provided project name", func() { tmp.InjectProjectName(name) Expect(tmp.ProjectName).To(Equal(name)) }) }) }) var _ = Describe("MultiGroupMixin", func() { var tmp mockTemplate BeforeEach(func() { tmp = mockTemplate{} }) Context("InjectMultiGroup", func() { It("should inject the provided multi group flag", func() { tmp.InjectMultiGroup(true) Expect(tmp.MultiGroup).To(BeTrue()) }) }) }) var _ = Describe("BoilerplateMixin", func() { const boilerplate = "Copyright" var tmp mockTemplate BeforeEach(func() { tmp = mockTemplate{} }) Context("InjectBoilerplate", func() { It("should inject the provided boilerplate", func() { tmp.InjectBoilerplate(boilerplate) Expect(tmp.Boilerplate).To(Equal(boilerplate)) }) }) }) var _ = Describe("ResourceMixin", func() { var ( res *resource.Resource tmp mockTemplate ) BeforeEach(func() { res = &resource.Resource{GVK: resource.GVK{ Group: "group", Domain: "my.domain", Version: "v1", Kind: "Kind", }} tmp = mockTemplate{} }) Context("InjectResource", func() { It("should inject the provided resource", func() { tmp.InjectResource(res) Expect(tmp.Resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) }) }) }) ================================================ FILE: pkg/machinery/scaffold.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 machinery import ( "bufio" "bytes" "errors" "fmt" log "log/slog" "os" "path/filepath" "slices" "strings" "text/template" "github.com/spf13/afero" "golang.org/x/tools/imports" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) const ( createOrUpdate = os.O_WRONLY | os.O_CREATE | os.O_TRUNC // DefaultDirectoryPermission and DefaultFilePermission are used so generated // files work in shared and container workflows. Use them when writing scaffolded // or config files for consistency. DefaultDirectoryPermission os.FileMode = 0o755 DefaultFilePermission os.FileMode = 0o644 ) var options = imports.Options{ Comments: true, TabIndent: true, TabWidth: 8, FormatOnly: true, } // Scaffold uses templates to scaffold new files type Scaffold struct { // fs allows to mock the file system for tests fs afero.Fs // permissions for new directories and files dirPerm os.FileMode filePerm os.FileMode // injector is used to provide several fields to the templates injector injector } // ScaffoldOption allows to provide optional arguments to the Scaffold type ScaffoldOption func(*Scaffold) // NewScaffold returns a new Scaffold with the provided plugins func NewScaffold(fs Filesystem, options ...ScaffoldOption) *Scaffold { s := &Scaffold{ fs: fs.FS, dirPerm: DefaultDirectoryPermission, filePerm: DefaultFilePermission, } for _, option := range options { option(s) } return s } // WithDirectoryPermissions sets the permissions for new directories func WithDirectoryPermissions(dirPerm os.FileMode) ScaffoldOption { return func(s *Scaffold) { s.dirPerm = dirPerm } } // WithFilePermissions sets the permissions for new files func WithFilePermissions(filePerm os.FileMode) ScaffoldOption { return func(s *Scaffold) { s.filePerm = filePerm } } // WithConfig provides the project configuration to the Scaffold func WithConfig(cfg config.Config) ScaffoldOption { return func(s *Scaffold) { s.injector.config = cfg if cfg != nil && cfg.GetRepository() != "" { imports.LocalPrefix = cfg.GetRepository() } } } // WithBoilerplate provides the boilerplate to the Scaffold func WithBoilerplate(boilerplate string) ScaffoldOption { return func(s *Scaffold) { s.injector.boilerplate = boilerplate } } // WithResource provides the resource to the Scaffold func WithResource(res *resource.Resource) ScaffoldOption { return func(s *Scaffold) { s.injector.resource = res } } // Execute writes to disk the provided files func (s *Scaffold) Execute(builders ...Builder) error { // Initialize the files files := make(map[string]*File, len(builders)) for _, builder := range builders { // Inject common fields s.injector.injectInto(builder) // Validate file builders if reqValBuilder, requiresValidation := builder.(RequiresValidation); requiresValidation { if err := reqValBuilder.Validate(); err != nil { return ValidateError{err} } } // Build models for Template builders if t, isTemplate := builder.(Template); isTemplate { if err := s.buildFileModel(t, files); err != nil { return err } } // Build models for Inserter builders if i, isInserter := builder.(Inserter); isInserter { if err := s.updateFileModel(i, files); err != nil { return err } } } // Persist the files to disk for _, f := range files { if err := s.writeFile(f); err != nil { return err } } return nil } // buildFileModel scaffolds a single file func (Scaffold) buildFileModel(t Template, models map[string]*File) error { // Set the template default values if err := t.SetTemplateDefaults(); err != nil { return SetTemplateDefaultsError{err} } path := t.GetPath() // Handle already existing models if _, found := models[path]; found { switch t.GetIfExistsAction() { case SkipFile: return nil case Error: return ModelAlreadyExistsError{path} case OverwriteFile: default: return UnknownIfExistsActionError{path, t.GetIfExistsAction()} } } b, err := doTemplate(t) if err != nil { return err } models[path] = &File{ Path: path, Contents: string(b), IfExistsAction: t.GetIfExistsAction(), } return nil } // doTemplate executes the template for a file using the input func doTemplate(t Template) ([]byte, error) { // Create a new template.Template using the type of the Template as the name temp := template.New(fmt.Sprintf("%T", t)) leftDelim, rightDelim := t.GetDelim() if leftDelim != "" && rightDelim != "" { temp.Delims(leftDelim, rightDelim) } // Set the function map to be used fm := DefaultFuncMap() if templateWithFuncMap, hasCustomFuncMap := t.(UseCustomFuncMap); hasCustomFuncMap { fm = templateWithFuncMap.GetFuncMap() } temp.Funcs(fm) // Set the template body if _, err := temp.Parse(t.GetBody()); err != nil { return nil, fmt.Errorf("failed to parse template: %w", err) } // Execute the template out := &bytes.Buffer{} if err := temp.Execute(out, t); err != nil { return nil, fmt.Errorf("failed to execute template: %w", err) } b := out.Bytes() // TODO(adirio): move go-formatting to write step // gofmt the imports if filepath.Ext(t.GetPath()) == ".go" { var err error if b, err = imports.Process(t.GetPath(), b, &options); err != nil { return nil, fmt.Errorf("failed to process template: %w", err) } } return b, nil } // updateFileModel updates a single file func (s Scaffold) updateFileModel(i Inserter, models map[string]*File) error { m, err := s.loadPreviousModel(i, models) if err != nil { if errors.Is(err, os.ErrNotExist) { if withOptionalBehavior, ok := i.(HasIfNotExistsAction); ok { switch withOptionalBehavior.GetIfNotExistsAction() { case IgnoreFile: log.Warn("skipping missing file", "file", i.GetPath()) log.Warn("the code fragments will not be inserted") return nil case ErrorIfNotExist: return err default: return err } } // If inserter doesn't implement HasIfNotExistsAction, return the original error return err } return fmt.Errorf("failed to load previous model for %s: %w", i.GetPath(), err) } // Get valid code fragments codeFragments := getValidCodeFragments(i) // Remove code fragments that already were applied err = filterExistingValues(m.Contents, codeFragments) if err != nil { return fmt.Errorf("failed to filter existing values: %w", err) } // If no code fragment to insert, we are done if len(codeFragments) == 0 { return nil } content, err := insertStrings(m.Contents, codeFragments) if err != nil { return fmt.Errorf("failed to insert values: %w", err) } // TODO(adirio): move go-formatting to write step formattedContent := content if ext := filepath.Ext(i.GetPath()); ext == ".go" { formattedContent, err = imports.Process(i.GetPath(), content, nil) if err != nil { return fmt.Errorf("failed to process formatted content: %w", err) } } m.Contents = string(formattedContent) m.IfExistsAction = OverwriteFile models[m.Path] = m return nil } // loadPreviousModel gets the previous model from the models map or the actual file func (s Scaffold) loadPreviousModel(i Inserter, models map[string]*File) (*File, error) { path := i.GetPath() // Let's see if we already have a model for this file if m, found := models[path]; found { // Check if there is already a scaffolded file exists, err := afero.Exists(s.fs, path) if err != nil { return nil, ExistsFileError{err} } // If there is a model but no scaffolded file we return the model if !exists { return m, nil } // If both a model and a file are found, check which has preference switch m.IfExistsAction { case SkipFile: // File has preference fromFile, err := s.loadModelFromFile(path) if err != nil { return m, nil } return fromFile, nil case Error: // Writing will result in an error, so we can return error now return nil, FileAlreadyExistsError{path} case OverwriteFile: // Model has preference return m, nil default: return nil, UnknownIfExistsActionError{path, m.IfExistsAction} } } // There was no model return s.loadModelFromFile(path) } // loadModelFromFile gets the previous model from the actual file func (s Scaffold) loadModelFromFile(path string) (f *File, err error) { reader, err := s.fs.Open(path) if err != nil { return nil, OpenFileError{err} } defer func() { if closeErr := reader.Close(); err == nil && closeErr != nil { err = CloseFileError{closeErr} } }() content, err := afero.ReadAll(reader) if err != nil { return nil, ReadFileError{err} } return &File{Path: path, Contents: string(content)}, nil } // getValidCodeFragments obtains the code fragments from a file.Inserter func getValidCodeFragments(i Inserter) CodeFragmentsMap { // Get the code fragments codeFragments := i.GetCodeFragments() // Validate the code fragments validMarkers := i.GetMarkers() for marker := range codeFragments { valid := slices.Contains(validMarkers, marker) if !valid { delete(codeFragments, marker) } } return codeFragments } // filterExistingValues removes code fragments that already exist in the content. func filterExistingValues(content string, codeFragmentsMap CodeFragmentsMap) error { for marker, codeFragments := range codeFragmentsMap { codeFragmentsOut := codeFragments[:0] for _, codeFragment := range codeFragments { exists, err := codeFragmentExists(content, codeFragment) if err != nil { return fmt.Errorf("failed to check if code fragment exists: %w", err) } if !exists { codeFragmentsOut = append(codeFragmentsOut, codeFragment) } } if len(codeFragmentsOut) == 0 { delete(codeFragmentsMap, marker) } else { codeFragmentsMap[marker] = codeFragmentsOut } } return nil } // codeFragmentExists checks if the codeFragment exists in the content. func codeFragmentExists(content, codeFragment string) (exists bool, err error) { // Trim space on each line in order to match different levels of indentation. var sb strings.Builder for line := range strings.SplitSeq(codeFragment, "\n") { _, _ = sb.WriteString(strings.TrimSpace(line)) _ = sb.WriteByte('\n') } codeFragmentTrimmed := strings.TrimSpace(sb.String()) scanLines := 1 + strings.Count(codeFragmentTrimmed, "\n") scanFunc := func(contentGroup string) bool { if contentGroup == codeFragmentTrimmed { exists = true return false } return true } if scanMultilineErr := scanMultiline(content, scanLines, scanFunc); scanMultilineErr != nil { return false, scanMultilineErr } return exists, nil } // scanMultiline scans a string while buffering the specified number of scanLines. It calls scanFunc // for every group of lines. The content passed to scanFunc will have trimmed whitespace. It // continues scanning the content as long as scanFunc returns true. func scanMultiline(content string, scanLines int, scanFunc func(contentGroup string) bool) error { scanner := bufio.NewScanner(strings.NewReader(content)) // Optimized simple case. if scanLines == 1 { for scanner.Scan() { if !scanFunc(strings.TrimSpace(scanner.Text())) { if err := scanner.Err(); err != nil { return fmt.Errorf("failed to scan content: %w", err) } return nil } } if err := scanner.Err(); err != nil { return fmt.Errorf("failed to scan content: %w", err) } return nil } // Complex case. bufferedLines := make([]string, scanLines) bufferedLinesIndex := 0 var sb strings.Builder for scanner.Scan() { // Trim space on each line in order to match different levels of indentation. bufferedLines[bufferedLinesIndex] = strings.TrimSpace(scanner.Text()) bufferedLinesIndex = (bufferedLinesIndex + 1) % scanLines sb.Reset() for i := range scanLines { _, _ = sb.WriteString(bufferedLines[(bufferedLinesIndex+i)%scanLines]) _ = sb.WriteByte('\n') } if !scanFunc(strings.TrimSpace(sb.String())) { if err := scanner.Err(); err != nil { return fmt.Errorf("failed to scan content: %w", err) } return nil } } if err := scanner.Err(); err != nil { return fmt.Errorf("failed to scan content: %w", err) } return nil } func insertStrings(content string, codeFragmentsMap CodeFragmentsMap) ([]byte, error) { out := new(bytes.Buffer) scanner := bufio.NewScanner(strings.NewReader(content)) for scanner.Scan() { line := scanner.Text() for marker, codeFragments := range codeFragmentsMap { if marker.EqualsLine(line) { for _, codeFragment := range codeFragments { _, _ = out.WriteString(codeFragment) // bytes.Buffer.WriteString always returns nil errors } } } _, _ = out.WriteString(line + "\n") // bytes.Buffer.WriteString always returns nil errors } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to scan content: %w", err) } return out.Bytes(), nil } func (s Scaffold) writeFile(f *File) error { // Check if the file to write already exists exists, err := afero.Exists(s.fs, f.Path) if err != nil { return ExistsFileError{err} } if exists { switch f.IfExistsAction { case OverwriteFile: // By not returning, the file is written as if it didn't exist case SkipFile: // By returning nil, the file is not written but the process will carry on return nil case Error: // By returning an error, the file is not written and the process will fail return FileAlreadyExistsError{f.Path} } } // Create the directory if needed if err = s.fs.MkdirAll(filepath.Dir(f.Path), s.dirPerm); err != nil { return CreateDirectoryError{err} } // Create or truncate the file writer, err := s.fs.OpenFile(f.Path, createOrUpdate, s.filePerm) if err != nil { return CreateFileError{err} } defer func() { if closeErr := writer.Close(); err == nil && closeErr != nil { err = CloseFileError{err} } }() if _, writeErr := writer.Write([]byte(f.Contents)); writeErr != nil { return WriteFileError{writeErr} } return nil } ================================================ FILE: pkg/machinery/scaffold_test.go ================================================ /* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package machinery import ( "errors" "os" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/afero" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) var _ = Describe("Scaffold", func() { Describe("NewScaffold", func() { It("should succeed for no option", func() { s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(DefaultDirectoryPermission)) Expect(s.filePerm).To(Equal(DefaultFilePermission)) Expect(s.injector.config).To(BeNil()) Expect(s.injector.boilerplate).To(Equal("")) Expect(s.injector.resource).To(BeNil()) }) It("should succeed with directory permissions option", func() { const dirPermissions os.FileMode = 0o755 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithDirectoryPermissions(dirPermissions)) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(dirPermissions)) Expect(s.filePerm).To(Equal(DefaultFilePermission)) Expect(s.injector.config).To(BeNil()) Expect(s.injector.boilerplate).To(Equal("")) Expect(s.injector.resource).To(BeNil()) }) It("should succeed with file permissions option", func() { const filePermissions os.FileMode = 0o755 s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithFilePermissions(filePermissions)) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(DefaultDirectoryPermission)) Expect(s.filePerm).To(Equal(filePermissions)) Expect(s.injector.config).To(BeNil()) Expect(s.injector.boilerplate).To(Equal("")) Expect(s.injector.resource).To(BeNil()) }) It("should succeed with config option", func() { cfg := cfgv3.New() s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithConfig(cfg)) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(DefaultDirectoryPermission)) Expect(s.filePerm).To(Equal(DefaultFilePermission)) Expect(s.injector.config).NotTo(BeNil()) Expect(s.injector.config.GetVersion().Compare(cfgv3.Version)).To(Equal(0)) Expect(s.injector.boilerplate).To(Equal("")) Expect(s.injector.resource).To(BeNil()) }) It("should succeed with boilerplate option", func() { const boilerplate = "Copyright" s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithBoilerplate(boilerplate)) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(DefaultDirectoryPermission)) Expect(s.filePerm).To(Equal(DefaultFilePermission)) Expect(s.injector.config).To(BeNil()) Expect(s.injector.boilerplate).To(Equal(boilerplate)) Expect(s.injector.resource).To(BeNil()) }) It("should succeed with resource option", func() { res := &resource.Resource{GVK: resource.GVK{ Group: "group", Domain: "my.domain", Version: "v1", Kind: "Kind", }} s := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithResource(res)) Expect(s.fs).NotTo(BeNil()) Expect(s.dirPerm).To(Equal(DefaultDirectoryPermission)) Expect(s.filePerm).To(Equal(DefaultFilePermission)) Expect(s.injector.config).To(BeNil()) Expect(s.injector.boilerplate).To(Equal("")) Expect(s.injector.resource).NotTo(BeNil()) Expect(s.injector.resource.GVK.IsEqualTo(res.GVK)).To(BeTrue()) }) }) Describe("Scaffold.Execute", func() { const ( path = "filename" pathGo = path + ".go" pathYaml = path + ".yaml" content = "Hello world!" ) var ( testErr error s *Scaffold ) BeforeEach(func() { testErr = errors.New("error text") s = &Scaffold{fs: afero.NewMemMapFs()} }) DescribeTable("successes", func(path, expected string, files ...Builder) { Expect(s.Execute(files...)).To(Succeed()) b, err := afero.ReadFile(s.fs, path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal(expected)) }, Entry("should write the file", path, content, &fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, ), Entry("should skip optional models if already have one", path, content, &fakeTemplate{fakeBuilder: fakeBuilder{path: path}, body: content}, &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, ), Entry("should overwrite required models if already have one", path, content, &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, &fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, body: content}, ), Entry("should format a go file", pathGo, "package file\n", &fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: "package file"}, ), Entry("should render actions correctly", path, "package testValue", &fakeTemplate{fakeBuilder: fakeBuilder{path: path, TestField: "testValue"}, body: "package {{.TestField}}"}, ), Entry("should render actions with alternative delimiters correctly", path, "package testValue", &fakeTemplate{ fakeBuilder: fakeBuilder{path: path, TestField: "testValue"}, body: "package [[.TestField]]", parseDelimLeft: "[[", parseDelimRight: "]]", }, ), ) DescribeTable("file builders related errors", func(setup func() ([]Builder, error)) { files, errType := setup() err := s.Execute(files...) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(errType)) }, Entry("should fail if unable to validate a file builder", func() ([]Builder, error) { return []Builder{ fakeRequiresValidation{validateErr: testErr}, }, ValidateError{testErr} }), Entry("should fail if unable to set default values for a template", func() ([]Builder, error) { return []Builder{ &fakeTemplate{err: testErr}, }, SetTemplateDefaultsError{testErr} }), Entry("should fail if an unexpected previous model is found", func() ([]Builder, error) { return []Builder{ &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, &fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}}, }, ModelAlreadyExistsError{path: path} }), Entry("should fail if behavior if-exists-action is not defined", func() ([]Builder, error) { return []Builder{ &fakeTemplate{fakeBuilder: fakeBuilder{path: path}}, &fakeTemplate{fakeBuilder: fakeBuilder{path: path, ifExistsAction: -1}}, }, UnknownIfExistsActionError{path: path, ifExistsAction: -1} }), ) // Following errors are unwrapped, so we need to check for substrings DescribeTable("template related errors", func(errMsg string, files ...Builder) { err := s.Execute(files...) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring(errMsg)) }, Entry("should fail if a template is broken", "template: ", &fakeTemplate{body: "{{ .Field }"}, ), Entry("should fail if a template params aren't provided", "template: ", &fakeTemplate{body: "{{ .Field }}"}, ), Entry("should fail if unable to format a go file", "expected 'package', found ", &fakeTemplate{fakeBuilder: fakeBuilder{path: pathGo}, body: content}, ), ) DescribeTable("insert strings", func(path, input, expected string, files ...Builder) { Expect(afero.WriteFile(s.fs, path, []byte(input), 0o666)).To(Succeed()) Expect(s.Execute(files...)).To(Succeed()) b, err := afero.ReadFile(s.fs, path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal(expected)) }, Entry("should insert lines for go files", pathGo, `package test // +kubebuilder:scaffold:- `, `package test var a int var b int // +kubebuilder:scaffold:- `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathGo}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathGo, "-"): {"var a int\n", "var b int\n"}, }, }, ), Entry("should insert lines for yaml files", pathYaml, ` # +kubebuilder:scaffold:- `, ` 1 2 # +kubebuilder:scaffold:- `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), Entry("should use models if there is no file", pathYaml, "", ` 1 2 # +kubebuilder:scaffold:- `, &fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` # +kubebuilder:scaffold:- `}, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), Entry("should use required models over files", pathYaml, content, ` 1 2 # +kubebuilder:scaffold:- `, &fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml, ifExistsAction: OverwriteFile}, body: ` # +kubebuilder:scaffold:- `}, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), Entry("should use files over optional models", pathYaml, ` # +kubebuilder:scaffold:- `, ` 1 2 # +kubebuilder:scaffold:- `, &fakeTemplate{fakeBuilder: fakeBuilder{path: pathYaml}, body: content}, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, }, }, ), Entry("should filter invalid markers", pathYaml, ` # +kubebuilder:scaffold:- # +kubebuilder:scaffold:* `, ` 1 2 # +kubebuilder:scaffold:- # +kubebuilder:scaffold:* `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, markers: []Marker{NewMarkerFor(pathYaml, "-")}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, }, }, ), Entry("should filter already existing one-line code fragments", pathYaml, ` 1 # +kubebuilder:scaffold:- 3 4 # +kubebuilder:scaffold:* `, ` 1 2 # +kubebuilder:scaffold:- 3 4 # +kubebuilder:scaffold:* `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathYaml, "-"): {"1\n", "2\n"}, NewMarkerFor(pathYaml, "*"): {"3\n", "4\n"}, }, }, ), Entry("should filter already existing multi-line indented code fragments", pathGo, `package test func init() { if err := something(); err != nil { return err } // +kubebuilder:scaffold:- } `, `package test func init() { if err := something(); err != nil { return err } // +kubebuilder:scaffold:- } `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathGo}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathGo, "-"): {"if err := something(); err != nil {\n\treturn err\n}\n\n"}, }, }, ), Entry("should not insert anything if no code fragment", pathYaml, ` # +kubebuilder:scaffold:- `, ` # +kubebuilder:scaffold:- `, fakeInserter{ fakeBuilder: fakeBuilder{path: pathYaml}, codeFragments: CodeFragmentsMap{ NewMarkerFor(pathYaml, "-"): {}, }, }, ), ) DescribeTable("insert strings related errors", func(errType error, files ...Builder) { Expect(afero.WriteFile(s.fs, path, []byte{}, 0o666)).To(Succeed()) err := s.Execute(files...) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(errType)) }, Entry("should fail if inserting into a model that fails when a file exists and it does exist", FileAlreadyExistsError{path: "filename"}, &fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: Error}}, fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, ), Entry("should fail if inserting into a model with unknown behavior if the file exists and it does exist", UnknownIfExistsActionError{path: "filename", ifExistsAction: -1}, &fakeTemplate{fakeBuilder: fakeBuilder{path: "filename", ifExistsAction: -1}}, fakeInserter{fakeBuilder: fakeBuilder{path: "filename"}}, ), ) Context("write when the file already exists", func() { BeforeEach(func() { _ = afero.WriteFile(s.fs, path, []byte{}, 0o666) }) It("should skip the file by default", func() { Expect(s.Execute(&fakeTemplate{ fakeBuilder: fakeBuilder{path: path}, body: content, })).To(Succeed()) b, err := afero.ReadFile(s.fs, path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(BeEmpty()) }) It("should write the file if configured to do so", func() { Expect(s.Execute(&fakeTemplate{ fakeBuilder: fakeBuilder{path: path, ifExistsAction: OverwriteFile}, body: content, })).To(Succeed()) b, err := afero.ReadFile(s.fs, path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal(content)) }) It("should error if configured to do so", func() { err := s.Execute(&fakeTemplate{ fakeBuilder: fakeBuilder{path: path, ifExistsAction: Error}, body: content, }) Expect(err).To(HaveOccurred()) Expect(err).To(MatchError(FileAlreadyExistsError{path: path})) }) }) Context("WithConfig option", func() { It("should set repository in imports.LocalPrefix", func() { cfg := cfgv3.New() _ = cfg.SetRepository("github.com/example/test") scaffold := NewScaffold(Filesystem{FS: afero.NewMemMapFs()}, WithConfig(cfg)) Expect(scaffold.injector.config).NotTo(BeNil()) Expect(scaffold.injector.config.GetRepository()).To(Equal("github.com/example/test")) }) }) Context("inserter with missing file", func() { It("should skip when IgnoreFile action is used", func() { err := s.Execute( fakeInserterWithIfNotExists{ fakeInserter: fakeInserter{ fakeBuilder: fakeBuilder{path: "missing.go"}, codeFragments: CodeFragmentsMap{ NewMarkerFor("missing.go", "-"): {"new code\n"}, }, }, ifNotExistsAction: IgnoreFile, }, ) Expect(err).NotTo(HaveOccurred()) }) It("should error when ErrorIfNotExist action is used", func() { err := s.Execute( fakeInserterWithIfNotExists{ fakeInserter: fakeInserter{ fakeBuilder: fakeBuilder{path: "missing.go"}, codeFragments: CodeFragmentsMap{ NewMarkerFor("missing.go", "-"): {"new code\n"}, }, }, ifNotExistsAction: ErrorIfNotExist, }, ) Expect(err).To(HaveOccurred()) }) }) }) }) var _ Builder = fakeBuilder{} // fakeBuilder is used to mock a Builder type fakeBuilder struct { path string ifExistsAction IfExistsAction TestField string // test go template actions } // GetPath implements Builder func (f fakeBuilder) GetPath() string { return f.path } // GetIfExistsAction implements Builder func (f fakeBuilder) GetIfExistsAction() IfExistsAction { return f.ifExistsAction } var _ RequiresValidation = fakeRequiresValidation{} // fakeRequiresValidation is used to mock a RequiresValidation in order to test Scaffold type fakeRequiresValidation struct { fakeBuilder validateErr error } // Validate implements RequiresValidation func (f fakeRequiresValidation) Validate() error { return f.validateErr } var _ Template = &fakeTemplate{} // fakeTemplate is used to mock a File in order to test Scaffold type fakeTemplate struct { fakeBuilder body string err error parseDelimLeft string parseDelimRight string } func (f *fakeTemplate) SetDelim(left, right string) { f.parseDelimLeft = left f.parseDelimRight = right } func (f *fakeTemplate) GetDelim() (string, string) { return f.parseDelimLeft, f.parseDelimRight } // GetBody implements Template func (f *fakeTemplate) GetBody() string { return f.body } // SetTemplateDefaults implements Template func (f *fakeTemplate) SetTemplateDefaults() error { if f.err != nil { return f.err } return nil } type fakeInserter struct { fakeBuilder markers []Marker codeFragments CodeFragmentsMap } // GetMarkers implements Inserter func (f fakeInserter) GetMarkers() []Marker { if f.markers != nil { return f.markers } markers := make([]Marker, 0, len(f.codeFragments)) for marker := range f.codeFragments { markers = append(markers, marker) } return markers } // GetCodeFragments implements Inserter func (f fakeInserter) GetCodeFragments() CodeFragmentsMap { return f.codeFragments } var ( _ Inserter = fakeInserterWithIfNotExists{} _ HasIfNotExistsAction = fakeInserterWithIfNotExists{} ) type fakeInserterWithIfNotExists struct { fakeInserter ifNotExistsAction IfNotExistsAction } func (f fakeInserterWithIfNotExists) GetIfNotExistsAction() IfNotExistsAction { return f.ifNotExistsAction } ================================================ FILE: pkg/model/resource/api.go ================================================ /* Copyright 2022 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 resource import ( "fmt" ) // API contains information about scaffolded APIs type API struct { // CRDVersion holds the CustomResourceDefinition API version used for the resource. CRDVersion string `json:"crdVersion,omitempty"` // Namespaced is true if the API is namespaced. Namespaced bool `json:"namespaced,omitempty"` } // Validate checks that the API is valid. func (api API) Validate() error { // Validate the CRD version if err := validateAPIVersion(api.CRDVersion); err != nil { return fmt.Errorf("invalid CRD version: %w", err) } return nil } // Copy returns a deep copy of the API that can be safely modified without affecting the original. func (api API) Copy() API { // As this function doesn't use a pointer receiver, api is already a shallow copy. // Any field that is a pointer, slice or map needs to be deep copied. return api } // Update combines fields of the APIs of two resources. func (api *API) Update(other *API) error { // If other is nil, nothing to merge if other == nil { return nil } // Update the version. if other.CRDVersion != "" { if api.CRDVersion == "" { api.CRDVersion = other.CRDVersion } else if api.CRDVersion != other.CRDVersion { return fmt.Errorf("CRD versions do not match") } } // Update the namespace. api.Namespaced = api.Namespaced || other.Namespaced return nil } // IsEmpty returns if the API's fields all contain zero-values. func (api API) IsEmpty() bool { return api.CRDVersion == "" && !api.Namespaced } ================================================ FILE: pkg/model/resource/api_test.go ================================================ /* Copyright 2022 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 resource import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) //nolint:dupl var _ = Describe("API", func() { Context("Validate", func() { It("should succeed for a valid API", func() { Expect(API{CRDVersion: v1}.Validate()).To(Succeed()) }) DescribeTable("should fail for invalid APIs", func(api API) { Expect(api.Validate()).NotTo(Succeed()) }, // Ensure that the rest of the fields are valid to check each part Entry("empty CRD version", API{}), Entry("invalid CRD version", API{CRDVersion: "1"}), ) }) Context("Update", func() { var api, other API It("should do nothing if provided a nil pointer", func() { api = API{} Expect(api.Update(nil)).To(Succeed()) Expect(api.CRDVersion).To(Equal("")) Expect(api.Namespaced).To(BeFalse()) api = API{ CRDVersion: v1, Namespaced: true, } Expect(api.Update(nil)).To(Succeed()) Expect(api.CRDVersion).To(Equal(v1)) Expect(api.Namespaced).To(BeTrue()) }) Context("CRD version", func() { It("should modify the CRD version if provided and not previously set", func() { api = API{} other = API{CRDVersion: v1} Expect(api.Update(&other)).To(Succeed()) Expect(api.CRDVersion).To(Equal(v1)) }) It("should keep the CRD version if not provided", func() { api = API{CRDVersion: v1} other = API{} Expect(api.Update(&other)).To(Succeed()) Expect(api.CRDVersion).To(Equal(v1)) }) It("should keep the CRD version if provided the same as previously set", func() { api = API{CRDVersion: v1} other = API{CRDVersion: v1} Expect(api.Update(&other)).To(Succeed()) Expect(api.CRDVersion).To(Equal(v1)) }) It("should fail if previously set and provided CRD versions do not match", func() { api = API{CRDVersion: v1} other = API{CRDVersion: "v1beta1"} Expect(api.Update(&other)).NotTo(Succeed()) }) }) Context("Namespaced", func() { It("should set the namespace scope if provided and not previously set", func() { api = API{} other = API{Namespaced: true} Expect(api.Update(&other)).To(Succeed()) Expect(api.Namespaced).To(BeTrue()) }) It("should keep the namespace scope if previously set", func() { api = API{Namespaced: true} By("not providing it") other = API{} Expect(api.Update(&other)).To(Succeed()) Expect(api.Namespaced).To(BeTrue()) By("providing it") other = API{Namespaced: true} Expect(api.Update(&other)).To(Succeed()) Expect(api.Namespaced).To(BeTrue()) }) It("should not set the namespace scope if not provided and not previously set", func() { api = API{} other = API{} Expect(api.Update(&other)).To(Succeed()) Expect(api.Namespaced).To(BeFalse()) }) }) }) Context("IsEmpty", func() { var ( none API cluster API namespaced API ) BeforeEach(func() { none = API{} cluster = API{ CRDVersion: v1, } namespaced = API{ CRDVersion: v1, Namespaced: true, } }) It("should return true fo an empty object", func() { Expect(none.IsEmpty()).To(BeTrue()) }) DescribeTable("should return false for non-empty objects", func(getAPI func() API) { Expect(getAPI().IsEmpty()).To(BeFalse()) }, Entry("cluster-scope", func() API { return cluster }), Entry("namespace-scope", func() API { return namespaced }), ) }) }) ================================================ FILE: pkg/model/resource/gvk.go ================================================ /* Copyright 2022 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 resource import ( "errors" "fmt" "strings" "k8s.io/apimachinery/pkg/util/validation" ) const ( versionInternal = "__internal" groupRequired = "group cannot be empty if the domain is empty" versionRequired = "version cannot be empty" kindRequired = "kind cannot be empty" ) // GVK stores the Group - Version - Kind triplet that uniquely identifies a resource. // In kubebuilder, the k8s fully qualified group is stored as Group and Domain to improve UX. type GVK struct { Group string `json:"group,omitempty"` Domain string `json:"domain,omitempty"` Version string `json:"version"` Kind string `json:"kind"` } // Validate checks that the GVK is valid. func (gvk GVK) Validate() error { // Check if the qualified group has a valid DNS1123 subdomain value if gvk.QualifiedGroup() == "" { return errors.New(groupRequired) } if err := validation.IsDNS1123Subdomain(gvk.QualifiedGroup()); err != nil { // NOTE: IsDNS1123Subdomain returns a slice of strings instead of an error, so no wrapping return fmt.Errorf("either Group or Domain is invalid: %s", err) } // Check if the version follows the valid pattern if gvk.Version == "" { return errors.New(versionRequired) } if errs := validation.IsDNS1123Subdomain(gvk.Version); len(errs) > 0 && gvk.Version != versionInternal { return fmt.Errorf("version must respect DNS-1123 (was %q)", gvk.Version) } // Check if kind has a valid DNS1035 label value if gvk.Kind == "" { return errors.New(kindRequired) } if errs := validation.IsDNS1035Label(strings.ToLower(gvk.Kind)); len(errs) != 0 { // NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping return fmt.Errorf("invalid Kind: %#v", errs) } // Require kind to start with an uppercase character // NOTE: previous validation already fails for empty strings, gvk.Kind[0] will not panic if string(gvk.Kind[0]) == strings.ToLower(string(gvk.Kind[0])) { return fmt.Errorf("invalid Kind: must start with an uppercase character") } return nil } // QualifiedGroup returns the fully qualified group name with the available information. func (gvk GVK) QualifiedGroup() string { switch "" { case gvk.Domain: return gvk.Group case gvk.Group: return gvk.Domain default: return fmt.Sprintf("%s.%s", gvk.Group, gvk.Domain) } } // IsEqualTo compares two GVK objects. func (gvk GVK) IsEqualTo(other GVK) bool { return gvk.Group == other.Group && gvk.Domain == other.Domain && gvk.Version == other.Version && gvk.Kind == other.Kind } ================================================ FILE: pkg/model/resource/gvk_test.go ================================================ /* Copyright 2022 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 resource import ( "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("GVK", func() { const ( group = "group" domain = "my.domain" version = "v1" kind = "Kind" internalVersion = "__internal" ) var gvk GVK BeforeEach(func() { gvk = GVK{Group: group, Domain: domain, Version: version, Kind: kind} }) Context("Validate", func() { DescribeTable("should pass valid GVKs", func(get func() GVK) { Expect(get().Validate()).To(Succeed()) }, Entry("Standard GVK", func() GVK { return gvk }), Entry("Version (__internal)", func() GVK { return GVK{Group: group, Domain: domain, Version: internalVersion, Kind: kind} }), ) DescribeTable("should fail for invalid GVKs", func(gvk GVK) { Expect(gvk.Validate()).NotTo(Succeed()) }, // Ensure that the rest of the fields are valid to check each part Entry("Group (uppercase)", GVK{Group: "Group", Domain: domain, Version: version, Kind: kind}), Entry("Group (non-alpha characters)", GVK{Group: "_*?", Domain: domain, Version: version, Kind: kind}), Entry("Domain (uppercase)", GVK{Group: group, Domain: "Domain", Version: version, Kind: kind}), Entry("Domain (non-alpha characters)", GVK{Group: group, Domain: "_*?", Version: version, Kind: kind}), Entry("Group and Domain (empty)", GVK{Group: "", Domain: "", Version: version, Kind: kind}), Entry("Version (empty)", GVK{Group: group, Domain: domain, Version: "", Kind: kind}), Entry("Version (wrong prefix)", GVK{Group: group, Domain: domain, Version: "-example.com", Kind: kind}), Entry("Version (wrong suffix)", GVK{Group: group, Domain: domain, Version: "example.com-", Kind: kind}), Entry("Version (uppercase)", GVK{Group: group, Domain: domain, Version: "Example.com", Kind: kind}), Entry("Version (special characters)", GVK{Group: group, Domain: domain, Version: "example!domain.com", Kind: kind}), Entry("Version (consecutive dots)", GVK{Group: group, Domain: domain, Version: "example..com", Kind: kind}), Entry("Kind (empty)", GVK{Group: group, Domain: domain, Version: version, Kind: ""}), Entry("Kind (whitespaces)", GVK{Group: group, Domain: domain, Version: version, Kind: "Ki nd"}), Entry("Kind (lowercase)", GVK{Group: group, Domain: domain, Version: version, Kind: "kind"}), Entry("Kind (starts with number)", GVK{Group: group, Domain: domain, Version: version, Kind: "1Kind"}), Entry("Kind (ends with `-`)", GVK{Group: group, Domain: domain, Version: version, Kind: "Kind-"}), Entry("Kind (non-alpha characters)", GVK{Group: group, Domain: domain, Version: version, Kind: "_*?"}), Entry("Kind (too long)", GVK{Group: group, Domain: domain, Version: version, Kind: strings.Repeat("a", 64)}), ) }) Context("QualifiedGroup", func() { DescribeTable("should return the correct string", func(get func() GVK, qualifiedGroup string) { Expect(get().QualifiedGroup()).To(Equal(qualifiedGroup)) }, Entry("fully qualified resource", func() GVK { return gvk }, group+"."+domain), Entry("empty group name", func() GVK { return GVK{Domain: domain, Version: version, Kind: kind} }, domain), Entry("empty domain", func() GVK { return GVK{Group: group, Version: version, Kind: kind} }, group), ) }) Context("IsEqualTo", func() { It("should return true for the same resource", func() { Expect(gvk.IsEqualTo(GVK{Group: group, Domain: domain, Version: version, Kind: kind})).To(BeTrue()) }) DescribeTable("should return false for different resources", func(get func() GVK) { Expect(gvk.IsEqualTo(get())).To(BeFalse()) }, Entry("different kind", func() GVK { return GVK{Group: group, Domain: domain, Version: version, Kind: "Kind2"} }), Entry("different version", func() GVK { return GVK{Group: group, Domain: domain, Version: "v2", Kind: kind} }), Entry("different domain", func() GVK { return GVK{Group: group, Domain: "other.domain", Version: version, Kind: kind} }), Entry("different group", func() GVK { return GVK{Group: "group2", Domain: domain, Version: version, Kind: kind} }), ) }) }) ================================================ FILE: pkg/model/resource/resource.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 resource import ( "fmt" "strings" "k8s.io/apimachinery/pkg/util/validation" ) // Resource contains the information required to scaffold files for a resource. type Resource struct { // GVK contains the resource's Group-Version-Kind triplet. GVK `json:",inline"` // Plural is the resource's kind plural form. Plural string `json:"plural,omitempty"` // Path is the path to the go package where the types are defined. Path string `json:"path,omitempty"` // API holds the information related to the resource API. API *API `json:"api,omitempty"` // Controller specifies if a controller has been scaffolded. Controller bool `json:"controller,omitempty"` // Webhooks holds the information related to the associated webhooks. Webhooks *Webhooks `json:"webhooks,omitempty"` // External specifies if the resource is defined externally. External bool `json:"external,omitempty"` // Module specifies the Go module path for external API dependencies. // Can optionally include @version to pin the dependency (e.g., "github.com/org/repo@v1.2.3"). // This is only used when External is true. Module string `json:"module,omitempty"` // Core specifies if the resource is from Kubernetes API. Core bool `json:"core,omitempty"` } // Validate checks that the Resource is valid. func (r Resource) Validate() error { // Validate the GVK if err := r.GVK.Validate(); err != nil { return err } // Validate the Plural // NOTE: IsDNS1035Label returns a slice of strings instead of an error, so no wrapping if errors := validation.IsDNS1035Label(r.Plural); len(errors) != 0 { return fmt.Errorf("invalid Plural: %#v", errors) } // TODO: validate the path // Validate the API if r.API != nil && !r.API.IsEmpty() { if err := r.API.Validate(); err != nil { return fmt.Errorf("invalid API: %w", err) } } // Validate the Webhooks if r.Webhooks != nil && !r.Webhooks.IsEmpty() { if err := r.Webhooks.Validate(); err != nil { return fmt.Errorf("invalid Webhooks: %w", err) } } return nil } // PackageName returns a name valid to be used por go packages. func (r Resource) PackageName() string { if r.Group == "" { return safeImport(r.Domain) } return safeImport(r.Group) } // ImportAlias returns a identifier usable as an import alias for this resource. func (r Resource) ImportAlias() string { if r.Group == "" { return safeImport(r.Domain + r.Version) } return safeImport(r.Group + r.Version) } // HasAPI returns true if the resource has an associated API. func (r Resource) HasAPI() bool { return r.API != nil && r.API.CRDVersion != "" } // HasController returns true if the resource has an associated controller. func (r Resource) HasController() bool { return r.Controller } // HasDefaultingWebhook returns true if the resource has an associated defaulting webhook. func (r Resource) HasDefaultingWebhook() bool { return r.Webhooks != nil && r.Webhooks.Defaulting } // HasValidationWebhook returns true if the resource has an associated validation webhook. func (r Resource) HasValidationWebhook() bool { return r.Webhooks != nil && r.Webhooks.Validation } // HasConversionWebhook returns true if the resource has an associated conversion webhook. func (r Resource) HasConversionWebhook() bool { return r.Webhooks != nil && r.Webhooks.Conversion } // IsExternal returns true if the resource was scaffold as external. func (r Resource) IsExternal() bool { return r.External } // IsRegularPlural returns true if the plural is the regular plural form for the kind. func (r Resource) IsRegularPlural() bool { return r.Plural == RegularPlural(r.Kind) } // Copy returns a deep copy of the Resource that can be safely modified without affecting the original. func (r Resource) Copy() Resource { // As this function doesn't use a pointer receiver, r is already a shallow copy. // Any field that is a pointer, slice or map needs to be deep copied. if r.API != nil { api := r.API.Copy() r.API = &api } if r.Webhooks != nil { webhooks := r.Webhooks.Copy() r.Webhooks = &webhooks } return r } // Update combines fields of two resources that have matching GVK favoring the receiver's values. func (r *Resource) Update(other Resource) error { // If self is nil, return an error if r == nil { return fmt.Errorf("cannot update a nil resource") } // Make sure we are not merging resources for different GVKs. if !r.IsEqualTo(other.GVK) { return fmt.Errorf("cannot update a resource (GVK %+v) with another with non-matching GVK %+v", r.GVK, other.GVK) } if r.Plural != other.Plural { return fmt.Errorf("cannot update resource (Plural %q) with another with non-matching Plural %q", r.Plural, other.Plural) } if other.Path != "" && r.Path != other.Path { if r.Path == "" { r.Path = other.Path } else { return fmt.Errorf("cannot update resource (Path %q) with another with non-matching Path %q", r.Path, other.Path) } } // Update API. if r.API == nil && other.API != nil { r.API = &API{} } if err := r.API.Update(other.API); err != nil { return err } // Update controller. r.Controller = r.Controller || other.Controller // Update Webhooks. if r.Webhooks == nil && other.Webhooks != nil { r.Webhooks = &Webhooks{} } return r.Webhooks.Update(other.Webhooks) } func wrapKey(key string) string { return fmt.Sprintf("%%[%s]", key) } // Replacer returns a strings.Replacer that replaces resource keywords with values. func (r Resource) Replacer() *strings.Replacer { replacements := make([]string, 0, 10) replacements = append(replacements, wrapKey("group"), r.Group) replacements = append(replacements, wrapKey("version"), r.Version) replacements = append(replacements, wrapKey("kind"), strings.ToLower(r.Kind)) replacements = append(replacements, wrapKey("plural"), strings.ToLower(r.Plural)) replacements = append(replacements, wrapKey("package-name"), r.PackageName()) return strings.NewReplacer(replacements...) } ================================================ FILE: pkg/model/resource/resource_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Resource", func() { const ( group = "group" domain = "test.io" version = "v1" kind = "Kind" plural = "kinds" v1beta1 = "v1beta1" ) var ( gvk GVK res Resource ) BeforeEach(func() { gvk = GVK{ Group: group, Domain: domain, Version: version, Kind: kind, } res = Resource{ GVK: gvk, Plural: plural, } }) Context("Validate", func() { It("should succeed for a valid Resource", func() { Expect(res.Validate()).To(Succeed()) }) It("should succeed with empty API", func() { Expect(Resource{GVK: gvk, Plural: plural, API: &API{}}.Validate()).To(Succeed()) }) It("should succeed with empty Webhooks", func() { Expect(Resource{GVK: gvk, Plural: plural, Webhooks: &Webhooks{}}.Validate()).To(Succeed()) }) It("should succeed with nil API", func() { Expect(Resource{GVK: gvk, Plural: plural, API: nil}.Validate()).To(Succeed()) }) It("should succeed with nil Webhooks", func() { Expect(Resource{GVK: gvk, Plural: plural, Webhooks: nil}.Validate()).To(Succeed()) }) It("should fail for invalid Plural with specific error", func() { r := Resource{GVK: gvk, Plural: "Plural"} err := r.Validate() Expect(err).NotTo(Succeed()) Expect(err.Error()).To(ContainSubstring("invalid Plural")) }) It("should fail for invalid API with wrapped error", func() { r := Resource{GVK: gvk, Plural: plural, API: &API{CRDVersion: "1"}} err := r.Validate() Expect(err).NotTo(Succeed()) Expect(err.Error()).To(ContainSubstring("invalid API")) }) It("should fail for invalid Webhooks with wrapped error", func() { r := Resource{GVK: gvk, Plural: plural, Webhooks: &Webhooks{WebhookVersion: "1"}} err := r.Validate() Expect(err).NotTo(Succeed()) Expect(err.Error()).To(ContainSubstring("invalid Webhooks")) }) DescribeTable("should fail for invalid Resources", func(res Resource) { Expect(res.Validate()).NotTo(Succeed()) }, // Ensure that the rest of the fields are valid to check each part Entry("invalid GVK", Resource{GVK: GVK{}, Plural: "plural"}), Entry("invalid Plural", Resource{GVK: gvk, Plural: "Plural"}), Entry("invalid API", Resource{GVK: gvk, Plural: "plural", API: &API{CRDVersion: "1"}}), Entry("invalid Webhooks", Resource{GVK: gvk, Plural: "plural", Webhooks: &Webhooks{WebhookVersion: "1"}}), ) }) Context("compound field", func() { const ( safeDomain = "testio" groupVersion = group + version domainVersion = safeDomain + version safeGroup = "mygroup" safeAlias = safeGroup + version ) var ( resNoGroup Resource resNoDomain Resource resHyphenGroup Resource resDotGroup Resource ) BeforeEach(func() { resNoGroup = Resource{ GVK: GVK{ // Empty group Domain: domain, Version: version, Kind: kind, }, } resNoDomain = Resource{ GVK: GVK{ Group: group, // Empty domain Version: version, Kind: kind, }, } resHyphenGroup = Resource{ GVK: GVK{ Group: "my-group", Domain: domain, Version: version, Kind: kind, }, } resDotGroup = Resource{ GVK: GVK{ Group: "my.group", Domain: domain, Version: version, Kind: kind, }, } }) DescribeTable("PackageName should return the correct string", func(getRes func() Resource, expected string) { Expect(getRes().PackageName()).To(Equal(expected)) }, Entry("fully qualified resource", func() Resource { return res }, group), Entry("empty group name", func() Resource { return resNoGroup }, safeDomain), Entry("empty domain", func() Resource { return resNoDomain }, group), Entry("hyphen-containing group", func() Resource { return resHyphenGroup }, safeGroup), Entry("dot-containing group", func() Resource { return resDotGroup }, safeGroup), ) DescribeTable("ImportAlias", func(getRes func() Resource, expected string) { Expect(getRes().ImportAlias()).To(Equal(expected)) }, Entry("fully qualified resource", func() Resource { return res }, groupVersion), Entry("empty group name", func() Resource { return resNoGroup }, domainVersion), Entry("empty domain", func() Resource { return resNoDomain }, groupVersion), Entry("hyphen-containing group", func() Resource { return resHyphenGroup }, safeAlias), Entry("dot-containing group", func() Resource { return resDotGroup }, safeAlias), ) }) Context("part check", func() { Context("HasAPI", func() { It("should return true if the API is scaffolded", func() { Expect(Resource{API: &API{CRDVersion: "v1"}}.HasAPI()).To(BeTrue()) }) DescribeTable("should return false if the API is not scaffolded", func(res Resource) { Expect(res.HasAPI()).To(BeFalse()) }, Entry("nil API", Resource{API: nil}), Entry("empty CRD version", Resource{API: &API{}}), ) }) Context("HasController", func() { It("should return true if the controller is scaffolded", func() { Expect(Resource{Controller: true}.HasController()).To(BeTrue()) }) It("should return false if the controller is not scaffolded", func() { Expect(Resource{Controller: false}.HasController()).To(BeFalse()) }) }) Context("HasDefaultingWebhook", func() { It("should return true if the defaulting webhook is scaffolded", func() { Expect(Resource{Webhooks: &Webhooks{Defaulting: true}}.HasDefaultingWebhook()).To(BeTrue()) }) DescribeTable("should return false if the defaulting webhook is not scaffolded", func(res Resource) { Expect(res.HasDefaultingWebhook()).To(BeFalse()) }, Entry("nil webhooks", Resource{Webhooks: nil}), Entry("no defaulting", Resource{Webhooks: &Webhooks{Defaulting: false}}), ) }) Context("HasValidationWebhook", func() { It("should return true if the validation webhook is scaffolded", func() { Expect(Resource{Webhooks: &Webhooks{Validation: true}}.HasValidationWebhook()).To(BeTrue()) }) DescribeTable("should return false if the validation webhook is not scaffolded", func(res Resource) { Expect(res.HasValidationWebhook()).To(BeFalse()) }, Entry("nil webhooks", Resource{Webhooks: nil}), Entry("no validation", Resource{Webhooks: &Webhooks{Validation: false}}), ) }) Context("HasConversionWebhook", func() { It("should return true if the conversion webhook is scaffolded", func() { Expect(Resource{Webhooks: &Webhooks{Conversion: true}}.HasConversionWebhook()).To(BeTrue()) }) DescribeTable("should return false if the conversion webhook is not scaffolded", func(res Resource) { Expect(res.HasConversionWebhook()).To(BeFalse()) }, Entry("nil webhooks", Resource{Webhooks: nil}), Entry("no conversion", Resource{Webhooks: &Webhooks{Conversion: false}}), ) }) Context("IsRegularPlural", func() { It("should return true if the regular plural form is used", func() { Expect(res.IsRegularPlural()).To(BeTrue()) }) It("should return false if an irregular plural form is used", func() { Expect(Resource{GVK: gvk, Plural: "types"}.IsRegularPlural()).To(BeFalse()) }) }) Context("IsExternal", func() { It("should return true if the resource is external", func() { Expect(Resource{External: true}.IsExternal()).To(BeTrue()) }) It("should return false if the resource is not external", func() { Expect(Resource{External: false}.IsExternal()).To(BeFalse()) }) }) }) Context("Copy", func() { const ( path = "api/v1" crdVersion = "v1" webhookVersion = "v1" ) BeforeEach(func() { res = Resource{ GVK: gvk, Plural: plural, Path: path, API: &API{ CRDVersion: crdVersion, Namespaced: true, }, Controller: true, Webhooks: &Webhooks{ WebhookVersion: webhookVersion, Defaulting: true, Validation: true, Conversion: true, }, } }) It("should return an exact copy", func() { other := res.Copy() Expect(other.Group).To(Equal(res.Group)) Expect(other.Domain).To(Equal(res.Domain)) Expect(other.Version).To(Equal(res.Version)) Expect(other.Kind).To(Equal(res.Kind)) Expect(other.Plural).To(Equal(res.Plural)) Expect(other.Path).To(Equal(res.Path)) Expect(other.API).NotTo(BeNil()) Expect(other.API.CRDVersion).To(Equal(res.API.CRDVersion)) Expect(other.API.Namespaced).To(Equal(res.API.Namespaced)) Expect(other.Controller).To(Equal(res.Controller)) Expect(other.Webhooks).NotTo(BeNil()) Expect(other.Webhooks.WebhookVersion).To(Equal(res.Webhooks.WebhookVersion)) Expect(other.Webhooks.Defaulting).To(Equal(res.Webhooks.Defaulting)) Expect(other.Webhooks.Validation).To(Equal(res.Webhooks.Validation)) Expect(other.Webhooks.Conversion).To(Equal(res.Webhooks.Conversion)) Expect(other.Webhooks.Spoke).To(Equal(res.Webhooks.Spoke)) }) It("modifying the copy should not affect the original", func() { other := res.Copy() other.Group = "group2" other.Domain = "other.domain" other.Version = "v2" other.Kind = "kind2" other.Plural = "kind2s" other.Path = "api/v2" other.API.CRDVersion = v1beta1 other.API.Namespaced = false other.API = nil // Change fields before changing pointer other.Controller = false other.Webhooks.WebhookVersion = v1beta1 other.Webhooks.Defaulting = false other.Webhooks.Validation = false other.Webhooks.Conversion = false other.Webhooks = nil // Change fields before changing pointer Expect(res.Group).To(Equal(group)) Expect(res.Domain).To(Equal(domain)) Expect(res.Version).To(Equal(version)) Expect(res.Kind).To(Equal(kind)) Expect(res.Plural).To(Equal(plural)) Expect(res.Path).To(Equal(path)) Expect(res.API).NotTo(BeNil()) Expect(res.API.CRDVersion).To(Equal(crdVersion)) Expect(res.API.Namespaced).To(BeTrue()) Expect(res.Controller).To(BeTrue()) Expect(res.Webhooks).NotTo(BeNil()) Expect(res.Webhooks.WebhookVersion).To(Equal(webhookVersion)) Expect(res.Webhooks.Defaulting).To(BeTrue()) Expect(res.Webhooks.Validation).To(BeTrue()) Expect(res.Webhooks.Conversion).To(BeTrue()) }) }) Context("Update", func() { var r, other Resource It("should fail for nil objects", func() { var nilResource *Resource Expect(nilResource.Update(other)).NotTo(Succeed()) }) It("should fail for different GVKs", func() { r = Resource{GVK: gvk} other = Resource{ GVK: GVK{ Group: group, Domain: domain, Version: version, Kind: "OtherKind", }, } Expect(r.Update(other)).NotTo(Succeed()) }) It("should fail for different Plurals", func() { r = Resource{ GVK: gvk, Plural: plural, } other = Resource{ GVK: gvk, Plural: "types", } Expect(r.Update(other)).NotTo(Succeed()) }) It("should work for a new path", func() { const path = "api/v1" r = Resource{GVK: gvk} other = Resource{ GVK: gvk, Path: path, } Expect(r.Update(other)).To(Succeed()) Expect(r.Path).To(Equal(path)) }) It("should fail for different Paths", func() { r = Resource{ GVK: gvk, Path: "api/v1", } other = Resource{ GVK: gvk, Path: "apis/group/v1", } Expect(r.Update(other)).NotTo(Succeed()) }) Context("API", func() { It("should work with nil APIs", func() { r = Resource{GVK: gvk} other = Resource{ GVK: gvk, API: &API{CRDVersion: v1}, } Expect(r.Update(other)).To(Succeed()) Expect(r.API).NotTo(BeNil()) Expect(r.API.CRDVersion).To(Equal(v1)) }) It("should fail if API.Update fails", func() { r = Resource{ GVK: gvk, API: &API{CRDVersion: v1}, } other = Resource{ GVK: gvk, API: &API{CRDVersion: v1beta1}, } Expect(r.Update(other)).NotTo(Succeed()) }) // The rest of the cases are tested in API.Update }) Context("Controller", func() { It("should set the controller flag if provided and not previously set", func() { r = Resource{GVK: gvk} other = Resource{ GVK: gvk, Controller: true, } Expect(r.Update(other)).To(Succeed()) Expect(r.Controller).To(BeTrue()) }) It("should keep the controller flag if previously set", func() { r = Resource{ GVK: gvk, Controller: true, } By("not providing it") other = Resource{GVK: gvk} Expect(r.Update(other)).To(Succeed()) Expect(r.Controller).To(BeTrue()) By("providing it") other = Resource{ GVK: gvk, Controller: true, } Expect(r.Update(other)).To(Succeed()) Expect(r.Controller).To(BeTrue()) }) It("should not set the controller flag if not provided and not previously set", func() { r = Resource{GVK: gvk} other = Resource{GVK: gvk} Expect(r.Update(other)).To(Succeed()) Expect(r.Controller).To(BeFalse()) }) }) Context("Webhooks", func() { It("should work with nil Webhooks", func() { r = Resource{GVK: gvk} other = Resource{ GVK: gvk, Webhooks: &Webhooks{WebhookVersion: v1}, } Expect(r.Update(other)).To(Succeed()) Expect(r.Webhooks).NotTo(BeNil()) Expect(r.Webhooks.WebhookVersion).To(Equal(v1)) }) It("should fail if Webhooks.Update fails", func() { r = Resource{ GVK: gvk, Webhooks: &Webhooks{WebhookVersion: v1}, } other = Resource{ GVK: gvk, Webhooks: &Webhooks{WebhookVersion: v1beta1}, } Expect(r.Update(other)).NotTo(Succeed()) }) // The rest of the cases are tested in Webhooks.Update }) }) Context("Replacer", func() { var replacer *strings.Replacer BeforeEach(func() { replacer = res.Replacer() }) DescribeTable("should replace the following strings", func(pattern string, expected func() string) { Expect(replacer.Replace(pattern)).To(Equal(expected())) }, Entry("no pattern", "version", func() string { return "version" }), Entry("pattern `%[group]`", "%[group]", func() string { return res.Group }), Entry("pattern `%[version]`", "%[version]", func() string { return res.Version }), Entry("pattern `%[kind]`", "%[kind]", func() string { return "kind" }), Entry("pattern `%[plural]`", "%[plural]", func() string { return res.Plural }), Entry("pattern `%[package-name]`", "%[package-name]", func() string { return res.PackageName() }), ) }) }) ================================================ FILE: pkg/model/resource/suite_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package resource import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) const v1 = "v1" func TestResource(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Resource Suite") } ================================================ FILE: pkg/model/resource/utils.go ================================================ /* Copyright 2022 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 resource import ( "fmt" "path" "strings" "github.com/gobuffalo/flect" ) // validateAPIVersion validates CRD or Webhook versions func validateAPIVersion(version string) error { switch version { case "v1": return nil default: return fmt.Errorf("API version must be one of: v1beta1, v1") } } // safeImport returns a cleaned version of the provided string that can be used for imports func safeImport(unsafe string) string { safe := unsafe // Remove dashes and dots safe = strings.ReplaceAll(safe, "-", "") safe = strings.ReplaceAll(safe, ".", "") return safe } // APIPackagePath returns the default path func APIPackagePath(repo, group, version string, multiGroup bool) string { if multiGroup && group != "" { return path.Join(repo, "api", group, version) } return path.Join(repo, "api", version) } // RegularPlural returns a default plural form when none was specified func RegularPlural(singular string) string { return flect.Pluralize(strings.ToLower(singular)) } ================================================ FILE: pkg/model/resource/utils_test.go ================================================ /* Copyright 2022 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 resource import ( "path" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = DescribeTable("safeImport should remove unsupported characters", func(unsafe, safe string) { Expect(safeImport(unsafe)).To(Equal(safe)) }, Entry("no dots nor dashes", "text", "text"), Entry("one dot", "my.domain", "mydomain"), Entry("several dots", "example.my.domain", "examplemydomain"), Entry("one dash", "example-text", "exampletext"), Entry("several dashes", "other-example-text", "otherexampletext"), Entry("both dots and dashes", "my-example.my.domain", "myexamplemydomain"), ) var _ = Describe("APIPackagePath", func() { const ( repo = "github.com/kubernetes-sigs/kubebuilder" group = "group" version = "v1" ) DescribeTable("should work", func(repo, group, version string, multiGroup bool, p string) { Expect(APIPackagePath(repo, group, version, multiGroup)).To(Equal(p)) }, Entry("single group setup", repo, group, version, false, path.Join(repo, "api", version)), Entry("multiple group setup", repo, group, version, true, path.Join(repo, "api", group, version)), Entry("multiple group setup with empty group", repo, "", version, true, path.Join(repo, "api", version)), ) }) var _ = DescribeTable("RegularPlural should return the regular plural form", func(singular, plural string) { Expect(RegularPlural(singular)).To(Equal(plural)) }, Entry("basic singular", "firstmate", "firstmates"), Entry("capitalized singular", "Firstmate", "firstmates"), Entry("camel-cased singular", "FirstMate", "firstmates"), Entry("irregular well-known plurals", "fish", "fish"), Entry("irregular well-known plurals", "helmswoman", "helmswomen"), ) ================================================ FILE: pkg/model/resource/webhooks.go ================================================ /* Copyright 2022 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 resource import ( "fmt" "slices" ) // Webhooks contains information about scaffolded webhooks type Webhooks struct { // WebhookVersion holds the {Validating,Mutating}WebhookConfiguration API version used for the resource. WebhookVersion string `json:"webhookVersion,omitempty"` // Defaulting specifies if a defaulting webhook is associated to the resource. Defaulting bool `json:"defaulting,omitempty"` // Validation specifies if a validation webhook is associated to the resource. Validation bool `json:"validation,omitempty"` // Conversion specifies if a conversion webhook is associated to the resource. Conversion bool `json:"conversion,omitempty"` Spoke []string `json:"spoke,omitempty"` // DefaultingPath holds the custom path for the defaulting/mutating webhook. // This path is used in the +kubebuilder:webhook marker annotation. DefaultingPath string `json:"defaultingPath,omitempty"` // ValidationPath holds the custom path for the validation webhook. // This path is used in the +kubebuilder:webhook marker annotation. ValidationPath string `json:"validationPath,omitempty"` } // Validate checks that the Webhooks is valid. func (webhooks Webhooks) Validate() error { // Validate the Webhook version if err := validateAPIVersion(webhooks.WebhookVersion); err != nil { return fmt.Errorf("invalid Webhook version: %w", err) } // Validate that Spoke versions are unique seen := map[string]bool{} for _, version := range webhooks.Spoke { if seen[version] { return fmt.Errorf("duplicate spoke version: %s", version) } seen[version] = true } return nil } // Copy returns a deep copy of the API that can be safely modified without affecting the original. func (webhooks Webhooks) Copy() Webhooks { // Deep copy the Spoke slice var spokeCopy []string if len(webhooks.Spoke) > 0 { spokeCopy = make([]string, len(webhooks.Spoke)) copy(spokeCopy, webhooks.Spoke) } else { spokeCopy = nil } return Webhooks{ WebhookVersion: webhooks.WebhookVersion, Defaulting: webhooks.Defaulting, Validation: webhooks.Validation, Conversion: webhooks.Conversion, Spoke: spokeCopy, DefaultingPath: webhooks.DefaultingPath, ValidationPath: webhooks.ValidationPath, } } // Update combines fields of the webhooks of two resources. func (webhooks *Webhooks) Update(other *Webhooks) error { // If other is nil, nothing to merge if other == nil { return nil } // Update the version. if other.WebhookVersion != "" { if webhooks.WebhookVersion == "" { webhooks.WebhookVersion = other.WebhookVersion } else if webhooks.WebhookVersion != other.WebhookVersion { return fmt.Errorf("webhook versions do not match") } } // Update defaulting. webhooks.Defaulting = webhooks.Defaulting || other.Defaulting // Update validation. webhooks.Validation = webhooks.Validation || other.Validation // Update conversion. webhooks.Conversion = webhooks.Conversion || other.Conversion // Update Spoke (merge without duplicates) if len(other.Spoke) > 0 { existingSpokes := make(map[string]struct{}) for _, spoke := range webhooks.Spoke { existingSpokes[spoke] = struct{}{} } for _, spoke := range other.Spoke { if _, exists := existingSpokes[spoke]; !exists { webhooks.Spoke = append(webhooks.Spoke, spoke) } } } // Update custom paths (other takes precedence if not empty) if other.DefaultingPath != "" { webhooks.DefaultingPath = other.DefaultingPath } if other.ValidationPath != "" { webhooks.ValidationPath = other.ValidationPath } return nil } // IsEmpty returns if the Webhooks' fields all contain zero-values. func (webhooks Webhooks) IsEmpty() bool { return webhooks.WebhookVersion == "" && !webhooks.Defaulting && !webhooks.Validation && !webhooks.Conversion && len(webhooks.Spoke) == 0 && webhooks.DefaultingPath == "" && webhooks.ValidationPath == "" } // AddSpoke adds a new spoke version to the Webhooks configuration. func (webhooks *Webhooks) AddSpoke(version string) { // Ensure the version is not already present if slices.Contains(webhooks.Spoke, version) { return } webhooks.Spoke = append(webhooks.Spoke, version) } ================================================ FILE: pkg/model/resource/webhooks_copy_test.go ================================================ /* Copyright 2025 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 resource import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Webhooks Copy and AddSpoke", func() { Context("Copy", func() { It("should create a deep copy of Webhooks", func() { original := Webhooks{ WebhookVersion: v1, Defaulting: true, Validation: true, Conversion: false, Spoke: []string{"v1", "v2"}, } copied := original.Copy() Expect(copied.WebhookVersion).To(Equal(original.WebhookVersion)) Expect(copied.Defaulting).To(Equal(original.Defaulting)) Expect(copied.Validation).To(Equal(original.Validation)) Expect(copied.Conversion).To(Equal(original.Conversion)) Expect(copied.Spoke).To(Equal(original.Spoke)) }) It("should not affect original when modifying the copy", func() { original := Webhooks{ WebhookVersion: v1, Defaulting: true, Spoke: []string{"v1"}, } copied := original.Copy() copied.Defaulting = false copied.Spoke = append(copied.Spoke, "v2") Expect(original.Defaulting).To(BeTrue()) Expect(original.Spoke).To(Equal([]string{"v1"})) Expect(copied.Defaulting).To(BeFalse()) Expect(copied.Spoke).To(Equal([]string{"v1", "v2"})) }) It("should handle empty Spoke slice", func() { original := Webhooks{ WebhookVersion: v1, Spoke: []string{}, } copied := original.Copy() Expect(copied.Spoke).To(BeNil()) }) It("should handle nil Spoke slice", func() { original := Webhooks{ WebhookVersion: v1, Spoke: nil, } copied := original.Copy() Expect(copied.Spoke).To(BeNil()) }) It("should create independent Spoke slices", func() { original := Webhooks{ Spoke: []string{"v1"}, } copied := original.Copy() copied.Spoke[0] = "v2" Expect(original.Spoke[0]).To(Equal("v1")) Expect(copied.Spoke[0]).To(Equal("v2")) }) }) Context("AddSpoke", func() { It("should add a new spoke version", func() { webhook := &Webhooks{} webhook.AddSpoke("v1") Expect(webhook.Spoke).To(HaveLen(1)) Expect(webhook.Spoke).To(ContainElement("v1")) }) It("should not add duplicate spoke versions", func() { webhook := &Webhooks{ Spoke: []string{"v1"}, } webhook.AddSpoke("v1") Expect(webhook.Spoke).To(HaveLen(1)) Expect(webhook.Spoke).To(Equal([]string{"v1"})) }) It("should add multiple different spoke versions", func() { webhook := &Webhooks{} webhook.AddSpoke("v1") webhook.AddSpoke("v2") webhook.AddSpoke("v3") Expect(webhook.Spoke).To(HaveLen(3)) Expect(webhook.Spoke).To(ContainElements("v1", "v2", "v3")) }) It("should handle adding existing version in the middle", func() { webhook := &Webhooks{ Spoke: []string{"v1", "v2", "v3"}, } webhook.AddSpoke("v2") Expect(webhook.Spoke).To(HaveLen(3)) Expect(webhook.Spoke).To(Equal([]string{"v1", "v2", "v3"})) }) }) Context("Validate with duplicate Spoke versions", func() { It("should fail validation with duplicate spoke versions", func() { webhook := Webhooks{ WebhookVersion: v1, Spoke: []string{"v1", "v1"}, } Expect(webhook.Validate()).NotTo(Succeed()) }) It("should succeed validation with unique spoke versions", func() { webhook := Webhooks{ WebhookVersion: v1, Spoke: []string{"v1", "v2", "v3"}, } Expect(webhook.Validate()).To(Succeed()) }) }) Context("IsEmpty with Spoke", func() { It("should return false when only Spoke is set", func() { webhook := Webhooks{ Spoke: []string{"v1"}, } Expect(webhook.IsEmpty()).To(BeFalse()) }) It("should return true when Spoke is empty array", func() { webhook := Webhooks{ Spoke: []string{}, } Expect(webhook.IsEmpty()).To(BeTrue()) }) }) }) ================================================ FILE: pkg/model/resource/webhooks_test.go ================================================ /* Copyright 2022 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 resource import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) //nolint:dupl var _ = Describe("Webhooks", func() { Context("Validate", func() { It("should succeed for a valid Webhooks", func() { Expect(Webhooks{WebhookVersion: v1}.Validate()).To(Succeed()) }) It("should succeed for valid Webhooks with unique spoke versions", func() { Expect(Webhooks{WebhookVersion: v1, Spoke: []string{"v1", "v2", "v3"}}.Validate()).To(Succeed()) }) DescribeTable("should fail for invalid Webhooks", func(webhooks Webhooks) { Expect(webhooks.Validate()).NotTo(Succeed()) }, // Ensure that the rest of the fields are valid to check each part Entry("empty webhook version", Webhooks{}), Entry("invalid webhook version", Webhooks{WebhookVersion: "1"}), Entry("duplicate spoke versions", Webhooks{WebhookVersion: v1, Spoke: []string{"v1", "v2", "v1"}}), ) }) Context("Update", func() { var webhook, other Webhooks It("should do nothing if provided a nil pointer", func() { webhook = Webhooks{} Expect(webhook.Update(nil)).To(Succeed()) Expect(webhook.WebhookVersion).To(Equal("")) Expect(webhook.Defaulting).To(BeFalse()) Expect(webhook.Validation).To(BeFalse()) Expect(webhook.Conversion).To(BeFalse()) webhook = Webhooks{ WebhookVersion: v1, Defaulting: true, Validation: true, Conversion: true, Spoke: []string{"v2"}, } Expect(webhook.Update(nil)).To(Succeed()) Expect(webhook.WebhookVersion).To(Equal(v1)) Expect(webhook.Defaulting).To(BeTrue()) Expect(webhook.Validation).To(BeTrue()) Expect(webhook.Conversion).To(BeTrue()) Expect(webhook.Spoke).To(Equal([]string{"v2"})) }) It("should merge Spoke values without duplicates", func() { webhook = Webhooks{ Spoke: []string{"v1"}, } other = Webhooks{ Spoke: []string{"v1", "v2"}, } Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Spoke).To(ConsistOf("v1", "v2")) // Ensure no duplicates }) Context("webhooks version", func() { It("should modify the webhooks version if provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{WebhookVersion: v1} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.WebhookVersion).To(Equal(v1)) }) It("should keep the webhooks version if not provided", func() { webhook = Webhooks{WebhookVersion: v1} other = Webhooks{} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.WebhookVersion).To(Equal(v1)) }) It("should keep the webhooks version if provided the same as previously set", func() { webhook = Webhooks{WebhookVersion: v1} other = Webhooks{WebhookVersion: v1} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.WebhookVersion).To(Equal(v1)) }) It("should fail if previously set and provided webhooks versions do not match", func() { webhook = Webhooks{WebhookVersion: v1} other = Webhooks{WebhookVersion: "v1beta1"} Expect(webhook.Update(&other)).NotTo(Succeed()) }) }) Context("Defaulting", func() { It("should set the defaulting webhook if provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{Defaulting: true} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Defaulting).To(BeTrue()) }) It("should keep the defaulting webhook if previously set", func() { webhook = Webhooks{Defaulting: true} By("not providing it") other = Webhooks{} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Defaulting).To(BeTrue()) By("providing it") other = Webhooks{Defaulting: true} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Defaulting).To(BeTrue()) }) It("should not set the defaulting webhook if not provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Defaulting).To(BeFalse()) }) }) Context("Validation", func() { It("should set the validation webhook if provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{Validation: true} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Validation).To(BeTrue()) }) It("should keep the validation webhook if previously set", func() { webhook = Webhooks{Validation: true} By("not providing it") other = Webhooks{} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Validation).To(BeTrue()) By("providing it") other = Webhooks{Validation: true} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Validation).To(BeTrue()) }) It("should not set the validation webhook if not provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Validation).To(BeFalse()) }) }) Context("Conversion", func() { It("should set the conversion webhook if provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{Conversion: true} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Conversion).To(BeTrue()) }) It("should keep the conversion webhook if previously set", func() { webhook = Webhooks{Conversion: true} By("not providing it") other = Webhooks{} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Conversion).To(BeTrue()) By("providing it") other = Webhooks{Conversion: true} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Conversion).To(BeTrue()) }) It("should not set the conversion webhook if not provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.Conversion).To(BeFalse()) }) }) Context("Custom webhook paths", func() { It("should set the defaulting path if provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{DefaultingPath: "/custom-defaulting"} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.DefaultingPath).To(Equal("/custom-defaulting")) }) It("should update the defaulting path if other provides a new one", func() { webhook = Webhooks{DefaultingPath: "/old-path"} other = Webhooks{DefaultingPath: "/new-path"} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.DefaultingPath).To(Equal("/new-path")) }) It("should set the validation path if provided and not previously set", func() { webhook = Webhooks{} other = Webhooks{ValidationPath: "/custom-validation"} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.ValidationPath).To(Equal("/custom-validation")) }) It("should update the validation path if other provides a new one", func() { webhook = Webhooks{ValidationPath: "/old-path"} other = Webhooks{ValidationPath: "/new-path"} Expect(webhook.Update(&other)).To(Succeed()) Expect(webhook.ValidationPath).To(Equal("/new-path")) }) }) }) Context("IsEmpty", func() { var ( none Webhooks defaulting Webhooks validation Webhooks conversion Webhooks defaultingAndValidation Webhooks defaultingAndConversion Webhooks validationAndConversion Webhooks all Webhooks ) BeforeEach(func() { none = Webhooks{} defaulting = Webhooks{ WebhookVersion: "v1", Defaulting: true, Validation: false, Conversion: false, } validation = Webhooks{ WebhookVersion: "v1", Defaulting: false, Validation: true, Conversion: false, } conversion = Webhooks{ WebhookVersion: "v1", Defaulting: false, Validation: false, Conversion: true, } defaultingAndValidation = Webhooks{ WebhookVersion: "v1", Defaulting: true, Validation: true, Conversion: false, } defaultingAndConversion = Webhooks{ WebhookVersion: "v1", Defaulting: true, Validation: false, Conversion: true, } validationAndConversion = Webhooks{ WebhookVersion: "v1", Defaulting: false, Validation: true, Conversion: true, } all = Webhooks{ WebhookVersion: "v1", Defaulting: true, Validation: true, Conversion: true, } }) It("should return true fo an empty object", func() { Expect(none.IsEmpty()).To(BeTrue()) }) DescribeTable("should return false for non-empty objects", func(get func() Webhooks) { Expect(get().IsEmpty()).To(BeFalse()) }, Entry("defaulting", func() Webhooks { return defaulting }), Entry("validation", func() Webhooks { return validation }), Entry("conversion", func() Webhooks { return conversion }), Entry("defaulting and validation", func() Webhooks { return defaultingAndValidation }), Entry("defaulting and conversion", func() Webhooks { return defaultingAndConversion }), Entry("validation and conversion", func() Webhooks { return validationAndConversion }), Entry("defaulting and validation and conversion", func() Webhooks { return all }), ) }) Context("AddSpoke", func() { It("should add a spoke version if not already present", func() { webhook := Webhooks{} webhook.AddSpoke("v1") Expect(webhook.Spoke).To(Equal([]string{"v1"})) webhook.AddSpoke("v2") Expect(webhook.Spoke).To(ConsistOf("v1", "v2")) }) It("should not add a duplicate spoke version", func() { webhook := Webhooks{Spoke: []string{"v1"}} webhook.AddSpoke("v1") Expect(webhook.Spoke).To(Equal([]string{"v1"})) }) }) Context("Copy", func() { It("should return an exact copy", func() { webhook := Webhooks{ WebhookVersion: v1, Defaulting: true, Validation: true, Conversion: true, Spoke: []string{"v1", "v2"}, DefaultingPath: "/custom-defaulting", ValidationPath: "/custom-validation", } other := webhook.Copy() Expect(other.WebhookVersion).To(Equal(webhook.WebhookVersion)) Expect(other.Defaulting).To(Equal(webhook.Defaulting)) Expect(other.Validation).To(Equal(webhook.Validation)) Expect(other.Conversion).To(Equal(webhook.Conversion)) Expect(other.Spoke).To(Equal(webhook.Spoke)) Expect(other.DefaultingPath).To(Equal(webhook.DefaultingPath)) Expect(other.ValidationPath).To(Equal(webhook.ValidationPath)) }) It("modifying the copy should not affect the original", func() { webhook := Webhooks{ WebhookVersion: v1, Defaulting: true, Validation: true, Conversion: true, Spoke: []string{"v1", "v2"}, DefaultingPath: "/custom-defaulting", ValidationPath: "/custom-validation", } other := webhook.Copy() // Modify the copy other.WebhookVersion = "v1beta1" other.Defaulting = false other.Validation = false other.Conversion = false other.Spoke[0] = "v3" other.Spoke = append(other.Spoke, "v4") other.DefaultingPath = "/new-defaulting" other.ValidationPath = "/new-validation" // Original should remain unchanged Expect(webhook.WebhookVersion).To(Equal(v1)) Expect(webhook.Defaulting).To(BeTrue()) Expect(webhook.Validation).To(BeTrue()) Expect(webhook.Conversion).To(BeTrue()) Expect(webhook.Spoke).To(Equal([]string{"v1", "v2"})) Expect(webhook.DefaultingPath).To(Equal("/custom-defaulting")) Expect(webhook.ValidationPath).To(Equal("/custom-validation")) }) It("should handle nil Spoke slice", func() { webhook := Webhooks{ WebhookVersion: v1, Spoke: nil, } other := webhook.Copy() Expect(other.Spoke).To(BeNil()) }) }) }) ================================================ FILE: pkg/model/stage/stage.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stage import ( "errors" ) var errInvalid = errors.New("invalid version stage") // Stage represents the stability of a version type Stage uint8 // Order Stage in decreasing degree of stability for comparison purposes. // Stable must be 0 so that it is the default Stage const ( // The order in this const declaration will be used to order version stages except for Stable // Stable should be used for plugins that are rarely changed in backwards-compatible ways, e.g. bug fixes. Stable Stage = iota // Beta should be used for plugins that may be changed in minor ways and are not expected to break between uses. Beta Stage = iota // Alpha should be used for plugins that are frequently changed and may break between uses. Alpha Stage = iota ) const ( alpha = "alpha" beta = "beta" stable = "" ) // ParseStage parses stage into a Stage, assuming it is one of the valid stages func ParseStage(stage string) (Stage, error) { var s Stage return s, s.Parse(stage) } // Parse parses stage inline, assuming it is one of the valid stages func (s *Stage) Parse(stage string) error { switch stage { case alpha: *s = Alpha case beta: *s = Beta case stable: *s = Stable default: return errInvalid } return nil } // String returns the string representation of s func (s Stage) String() string { switch s { case Alpha: return alpha case Beta: return beta case Stable: return stable default: panic(errInvalid) } } // Validate ensures that the stage is one of the valid stages func (s Stage) Validate() error { switch s { case Alpha: case Beta: case Stable: default: return errInvalid } return nil } // Compare returns -1 if s < other, 0 if s == other, and 1 if s > other. func (s Stage) Compare(other Stage) int { if s == other { return 0 } // Stage are sorted in decreasing order if s > other { return -1 } return 1 } // IsStable returns whether the stage is stable or not func (s Stage) IsStable() bool { return s == Stable } ================================================ FILE: pkg/model/stage/stage_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package stage import ( "slices" "testing" . "github.com/onsi/ginkgo/v2" // An alias is required because Context is defined elsewhere in this package. . "github.com/onsi/gomega" ) func TestStage(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Stage Suite") } var _ = Describe("ParseStage", func() { DescribeTable("should be correctly parsed for valid stage strings", func(str string, stage Stage) { s, err := ParseStage(str) Expect(err).NotTo(HaveOccurred()) Expect(s).To(Equal(stage)) }, Entry("for alpha stage", "alpha", Alpha), Entry("for beta stage", "beta", Beta), Entry("for stable stage", "", Stable), ) DescribeTable("should error when parsing invalid stage strings", func(str string) { _, err := ParseStage(str) Expect(err).To(HaveOccurred()) }, Entry("passing a number as the stage string", "1"), Entry("passing `gamma` as the stage string", "gamma"), Entry("passing a dash-prefixed stage string", "-alpha"), ) }) var _ = Describe("Stage", func() { Context("String", func() { DescribeTable("should return the correct string value", func(stage Stage, str string) { Expect(stage.String()).To(Equal(str)) }, Entry("for alpha stage", Alpha, "alpha"), Entry("for beta stage", Beta, "beta"), Entry("for stable stage", Stable, ""), ) DescribeTable("should panic", func(stage Stage) { Expect(func() { _ = stage.String() }).To(Panic()) }, Entry("for stage 34", Stage(34)), Entry("for stage 75", Stage(75)), Entry("for stage 123", Stage(123)), Entry("for stage 255", Stage(255)), ) }) Context("Validate", func() { DescribeTable("should validate existing stages", func(stage Stage) { Expect(stage.Validate()).To(Succeed()) }, Entry("for alpha stage", Alpha), Entry("for beta stage", Beta), Entry("for stable stage", Stable), ) DescribeTable("should fail for non-existing stages", func(stage Stage) { Expect(stage.Validate()).NotTo(Succeed()) }, Entry("for stage 34", Stage(34)), Entry("for stage 75", Stage(75)), Entry("for stage 123", Stage(123)), Entry("for stage 255", Stage(255)), ) }) Context("Compare", func() { // Test Stage.Compare by sorting a list var ( stages []Stage sortedStages []Stage ) BeforeEach(func() { stages = []Stage{ Stable, Alpha, Stable, Beta, Beta, Alpha, } sortedStages = []Stage{ Alpha, Alpha, Beta, Beta, Stable, Stable, } }) It("sorts stages correctly", func() { slices.SortStableFunc(stages, func(a, b Stage) int { return a.Compare(b) }) Expect(stages).To(Equal(sortedStages)) }) }) Context("IsStable", func() { It("should return true for stable stage", func() { Expect(Stable.IsStable()).To(BeTrue()) }) DescribeTable("should return false for any unstable stage", func(stage Stage) { Expect(stage.IsStable()).To(BeFalse()) }, Entry("for alpha stage", Alpha), Entry("for beta stage", Beta), ) }) }) ================================================ FILE: pkg/plugin/bundle.go ================================================ /* Copyright 2022 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 plugin import ( "fmt" "sigs.k8s.io/kubebuilder/v4/pkg/config" ) type bundle struct { name string version Version plugins []Plugin description string supportedProjectVersions []config.Version deprecateWarning string } // BundleOption define the options to create the bundle type BundleOption func(*bundle) // WithName allow set the name of the Bundle Plugin func WithName(name string) BundleOption { return func(opts *bundle) { opts.name = name } } // WithVersion allow set the version of the Bundle Plugin func WithVersion(version Version) BundleOption { return func(opts *bundle) { opts.version = version } } // WithPlugins allow set the plugins which will be used in the composition for the Bundle Plugin func WithPlugins(plugins ...Plugin) BundleOption { return func(opts *bundle) { opts.plugins = plugins } } // WithDeprecationMessage allow set a deprecate message when needed func WithDeprecationMessage(msg string) BundleOption { return func(opts *bundle) { opts.deprecateWarning = msg } } // WithDescription allows setting a description for the bundle func WithDescription(desc string) BundleOption { return func(opts *bundle) { opts.description = desc } } // NewBundleWithOptions creates a new Bundle with the provided BundleOptions. // The list of supported project versions is computed from the provided plugins in options. func NewBundleWithOptions(opts ...BundleOption) (Bundle, error) { bundleOpts := bundle{} for _, opts := range opts { opts(&bundleOpts) } supportedProjectVersions := CommonSupportedProjectVersions(bundleOpts.plugins...) if len(supportedProjectVersions) == 0 { return nil, fmt.Errorf("in order to bundle plugins, they must all support at least one common project version") } // Plugins may be bundles themselves, so unbundle here // NOTE(Adirio): unbundling here ensures that Bundle.Plugin always returns a flat list of Plugins instead of also // including Bundles, and therefore we don't have to use a recursive algorithm when resolving. allPlugins := make([]Plugin, 0, len(bundleOpts.plugins)) for _, plugin := range bundleOpts.plugins { if pluginBundle, isBundle := plugin.(Bundle); isBundle { allPlugins = append(allPlugins, pluginBundle.Plugins()...) } else { allPlugins = append(allPlugins, plugin) } } return bundle{ name: bundleOpts.name, version: bundleOpts.version, plugins: allPlugins, description: bundleOpts.description, supportedProjectVersions: supportedProjectVersions, deprecateWarning: bundleOpts.deprecateWarning, }, nil } // Name implements Plugin func (b bundle) Name() string { return b.name } // Version implements Plugin func (b bundle) Version() Version { return b.version } // SupportedProjectVersions implements Plugin func (b bundle) SupportedProjectVersions() []config.Version { return b.supportedProjectVersions } // Plugins implements Bundle func (b bundle) Plugins() []Plugin { return b.plugins } // Description implements Describable func (b bundle) Description() string { return b.description } // DeprecationWarning return the warning message func (b bundle) DeprecationWarning() string { return b.deprecateWarning } ================================================ FILE: pkg/plugin/bundle_test.go ================================================ /* Copyright 2022 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 plugin import ( "slices" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" ) var _ = Describe("Bundle", func() { const ( name = "bundle.kubebuilder.io" ) var ( v Version p1 mockPlugin p2 mockPlugin p3 mockPlugin p4 mockPlugin ) BeforeEach(func() { v = Version{Number: 1} p1 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 1}, {Number: 2}, {Number: 3}, }} p2 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 1}, {Number: 2, Stage: stage.Beta}, {Number: 3, Stage: stage.Alpha}, }} p3 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 1}, {Number: 2}, {Number: 3, Stage: stage.Beta}, }} p4 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 2}, {Number: 3}, }} }) Context("NewBundle", func() { It("should succeed for plugins with common supported project versions", func() { for _, plugins := range [][]Plugin{ {p1, p2}, {p1, p3}, {p1, p4}, {p2, p3}, {p3, p4}, {p1, p2, p3}, {p1, p3, p4}, } { b, err := NewBundleWithOptions(WithName(name), WithVersion(v), WithPlugins(plugins...)) Expect(err).NotTo(HaveOccurred()) Expect(b.Name()).To(Equal(name)) Expect(b.Version().Compare(v)).To(Equal(0)) versions := b.SupportedProjectVersions() slices.SortStableFunc(versions, func(a, b config.Version) int { return a.Compare(b) }) expectedVersions := CommonSupportedProjectVersions(plugins...) slices.SortStableFunc(expectedVersions, func(a, b config.Version) int { return a.Compare(b) }) Expect(versions).To(Equal(expectedVersions)) Expect(b.Plugins()).To(Equal(plugins)) } }) It("should accept bundles as input", func() { var a, b Bundle var err error plugins := []Plugin{p1, p2, p3} a, err = NewBundleWithOptions(WithName("a"), WithVersion(v), WithPlugins(p1, p2)) Expect(err).NotTo(HaveOccurred()) b, err = NewBundleWithOptions(WithName("b"), WithVersion(v), WithPlugins(a, p3)) Expect(err).NotTo(HaveOccurred()) versions := b.SupportedProjectVersions() slices.SortStableFunc(versions, func(a, b config.Version) int { return a.Compare(b) }) expectedVersions := CommonSupportedProjectVersions(plugins...) slices.SortStableFunc(expectedVersions, func(a, b config.Version) int { return a.Compare(b) }) Expect(versions).To(Equal(expectedVersions)) Expect(b.Plugins()).To(Equal(plugins)) }) It("should fail for plugins with no common supported project version", func() { for _, plugins := range [][]Plugin{ {p2, p4}, {p1, p2, p4}, {p2, p3, p4}, {p1, p2, p3, p4}, } { _, err := NewBundleWithOptions(WithName(name), WithVersion(v), WithPlugins(plugins...)) Expect(err).To(HaveOccurred()) } }) }) Context("NewBundleWithOptions", func() { It("should succeed for plugins with common supported project versions", func() { for _, plugins := range [][]Plugin{ {p1, p2}, {p1, p3}, {p1, p4}, {p2, p3}, {p3, p4}, {p1, p2, p3}, {p1, p3, p4}, } { b, err := NewBundleWithOptions(WithName(name), WithVersion(v), WithDeprecationMessage(""), WithPlugins(plugins...), ) Expect(err).NotTo(HaveOccurred()) Expect(b.Name()).To(Equal(name)) Expect(b.Version().Compare(v)).To(Equal(0)) versions := b.SupportedProjectVersions() slices.SortStableFunc(versions, func(a, b config.Version) int { return a.Compare(b) }) expectedVersions := CommonSupportedProjectVersions(plugins...) slices.SortStableFunc(expectedVersions, func(a, b config.Version) int { return a.Compare(b) }) Expect(versions).To(Equal(expectedVersions)) Expect(b.Plugins()).To(Equal(plugins)) } }) It("should accept bundles as input", func() { var a, b Bundle var err error plugins := []Plugin{p1, p2, p3} a, err = NewBundleWithOptions(WithName("a"), WithVersion(v), WithDeprecationMessage(""), WithPlugins(p1, p2), ) Expect(err).NotTo(HaveOccurred()) b, err = NewBundleWithOptions(WithName("b"), WithVersion(v), WithDeprecationMessage(""), WithPlugins(a, p3), ) Expect(err).NotTo(HaveOccurred()) versions := b.SupportedProjectVersions() slices.SortStableFunc(versions, func(a, b config.Version) int { return a.Compare(b) }) expectedVersions := CommonSupportedProjectVersions(plugins...) slices.SortStableFunc(expectedVersions, func(a, b config.Version) int { return a.Compare(b) }) Expect(versions).To(Equal(expectedVersions)) Expect(b.Plugins()).To(Equal(plugins)) }) It("should fail for plugins with no common supported project version", func() { for _, plugins := range [][]Plugin{ {p2, p4}, {p1, p2, p4}, {p2, p3, p4}, {p1, p2, p3, p4}, } { _, err := NewBundleWithOptions(WithName(name), WithVersion(v), WithDeprecationMessage(""), WithPlugins(plugins...), ) Expect(err).To(HaveOccurred()) } }) It("should store and return the deprecation warning", func() { deprecationMsg := "This bundle is deprecated, please use v2" b, err := NewBundleWithOptions( WithName(name), WithVersion(v), WithDeprecationMessage(deprecationMsg), WithPlugins(p1), ) Expect(err).NotTo(HaveOccurred()) deprecated, ok := b.(Deprecated) Expect(ok).To(BeTrue()) Expect(deprecated.DeprecationWarning()).To(Equal(deprecationMsg)) }) It("should return empty string when no deprecation warning is set", func() { b, err := NewBundleWithOptions( WithName(name), WithVersion(v), WithPlugins(p1), ) Expect(err).NotTo(HaveOccurred()) deprecated, ok := b.(Deprecated) Expect(ok).To(BeTrue()) Expect(deprecated.DeprecationWarning()).To(BeEmpty()) }) }) }) ================================================ FILE: pkg/plugin/errors.go ================================================ /* Copyright 2022 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 plugin import ( "fmt" ) // ExitError is a typed error that is returned by a plugin when no further steps should be executed for itself. type ExitError struct { Plugin string Reason string } // Error implements error func (e ExitError) Error() string { return fmt.Sprintf("plugin %q exit early: %s", e.Plugin, e.Reason) } ================================================ FILE: pkg/plugin/errors_test.go ================================================ /* Copyright 2022 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 plugin import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("PluginKeyNotFoundError", func() { var err ExitError BeforeEach(func() { err = ExitError{ Plugin: "go.kubebuilder.io/v1", Reason: "skipping plugin", } }) Context("Error", func() { It("should return the correct error message", func() { Expect(err.Error()).To(Equal("plugin \"go.kubebuilder.io/v1\" exit early: skipping plugin")) }) }) }) ================================================ FILE: pkg/plugin/external/types.go ================================================ /* Copyright 2022 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 external import "sigs.k8s.io/kubebuilder/v4/pkg/plugin" // PluginRequest contains all information kubebuilder received from the CLI // and plugins executed before it. type PluginRequest struct { // APIVersion defines the versioned schema of PluginRequest that is being sent from Kubebuilder. // Initially, this will be marked as alpha (v1alpha1). APIVersion string `json:"apiVersion"` // Args holds the plugin specific arguments that are received from the CLI // which are to be passed down to the external plugin. Args []string `json:"args"` // Command contains the command to be executed by the plugin such as init, create api, etc. Command string `json:"command"` // Universe represents the modified file contents that gets updated over a series of plugin runs // across the plugin chain. Initially, it starts out as empty. Universe map[string]string `json:"universe"` // PluginChain contains the full plugin chain being used for this project. // This allows external plugins to know which other plugins are in use. // Format: ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"] PluginChain []string `json:"pluginChain,omitempty"` // Config contains the PROJECT file config. This field may be empty if the // project is being initialized and the PROJECT file has not been created yet. Config map[string]any `json:"config,omitempty"` } // PluginResponse is returned to kubebuilder by the plugin and contains all files // written by the plugin following a certain command. type PluginResponse struct { // APIVersion defines the versioned schema of the PluginResponse that is back sent back to Kubebuilder. // Initially, this will be marked as alpha (v1alpha1) APIVersion string `json:"apiVersion"` // Command holds the command that gets executed by the plugin such as init, create api, etc. Command string `json:"command"` // Metadata contains the plugin specific help text that the plugin returns to Kubebuilder when it receives // `--help` flag from Kubebuilder. Metadata plugin.SubcommandMetadata `json:"metadata"` // Universe in the PluginResponse represents the updated file contents that was written by the plugin. Universe map[string]string `json:"universe"` // Error is a boolean type that indicates whether there were any errors due to plugin failures. Error bool `json:"error,omitempty"` // ErrorMsgs contains the specific error messages of the plugin failures. ErrorMsgs []string `json:"errorMsgs,omitempty"` // Flags contains the plugin specific flags that the plugin returns to Kubebuilder when it receives // a request for a list of supported flags from Kubebuilder Flags []Flag `json:"flags,omitempty"` } // Flag is meant to represent a CLI flag that is used by Kubebuilder to define flags that are parsed // for use with an external plugin type Flag struct { // Name is the name that should be used when creating the flag. // i.e a name of "domain" would become the CLI flag "--domain" Name string // Type is the type of flag that should be created. The types that // Kubebuilder supports are: string, bool, int, and float. // any value other than the supported will be defaulted to be a string Type string // Default is the default value that should be used for a flag. // Kubebuilder will attempt to convert this value to the defined // type for this flag. Default string // Usage is a description of the flag and when/why/what it is used for. Usage string } ================================================ FILE: pkg/plugin/filter.go ================================================ /* Copyright 2022 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 plugin import ( "strings" "sigs.k8s.io/kubebuilder/v4/pkg/config" ) // FilterPluginsByKey returns the set of plugins that match the provided key (may be not-fully qualified) func FilterPluginsByKey(plugins []Plugin, key string) ([]Plugin, error) { name, ver := SplitKey(key) hasVersion := ver != "" var version Version if hasVersion { if err := version.Parse(ver); err != nil { return nil, err } } filtered := make([]Plugin, 0, len(plugins)) for _, plugin := range plugins { if !strings.HasPrefix(plugin.Name(), name) { continue } if hasVersion && plugin.Version().Compare(version) != 0 { continue } filtered = append(filtered, plugin) } return filtered, nil } // FilterPluginsByProjectVersion returns the set of plugins that support the provided project version func FilterPluginsByProjectVersion(plugins []Plugin, projectVersion config.Version) []Plugin { filtered := make([]Plugin, 0, len(plugins)) for _, plugin := range plugins { for _, supportedVersion := range plugin.SupportedProjectVersions() { if supportedVersion.Compare(projectVersion) == 0 { filtered = append(filtered, plugin) break } } } return filtered } ================================================ FILE: pkg/plugin/filter_test.go ================================================ /* Copyright 2022 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 plugin import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" ) var _ = Describe("FilterPlugins", func() { var ( p1 mockPlugin p2 mockPlugin p3 mockPlugin p4 mockPlugin p5 mockPlugin allPlugins []Plugin ) BeforeEach(func() { p1 = mockPlugin{ name: "go.kubebuilder.io", version: Version{Number: 2}, supportedProjectVersions: []config.Version{{Number: 2}, {Number: 3}}, } p2 = mockPlugin{ name: "go.kubebuilder.io", version: Version{Number: 3}, supportedProjectVersions: []config.Version{{Number: 3}}, } p3 = mockPlugin{ name: "example.kubebuilder.io", version: Version{Number: 1}, supportedProjectVersions: []config.Version{{Number: 2}}, } p4 = mockPlugin{ name: "test.kubebuilder.io", version: Version{Number: 1}, supportedProjectVersions: []config.Version{{Number: 3}}, } p5 = mockPlugin{ name: "go.test.domain", version: Version{Number: 2}, supportedProjectVersions: []config.Version{{Number: 2}}, } allPlugins = []Plugin{p1, p2, p3, p4, p5} }) DescribeTable("should filter by key", func(key string, expectedPlugins func() []Plugin) { filtered, err := FilterPluginsByKey(allPlugins, key) Expect(err).NotTo(HaveOccurred()) Expect(filtered).To(Equal(expectedPlugins())) }, Entry("go plugins", "go", func() []Plugin { return []Plugin{p1, p2, p5} }), Entry("go plugins (kubebuilder domain)", "go.kubebuilder", func() []Plugin { return []Plugin{p1, p2} }), Entry("go v2 plugins", "go/v2", func() []Plugin { return []Plugin{p1, p5} }), Entry("go v2 plugins (kubebuilder domain)", "go.kubebuilder/v2", func() []Plugin { return []Plugin{p1} }), ) It("should fail for invalid versions", func() { _, err := FilterPluginsByKey(allPlugins, "go/a") Expect(err).To(HaveOccurred()) }) DescribeTable("should filter by project version", func(projectVersion config.Version, expectedPlugins func() []Plugin) { Expect(FilterPluginsByProjectVersion(allPlugins, projectVersion)).To(Equal(expectedPlugins())) }, Entry("project v2 plugins", config.Version{Number: 2}, func() []Plugin { return []Plugin{p1, p3, p5} }), Entry("project v3 plugins", config.Version{Number: 3}, func() []Plugin { return []Plugin{p1, p2, p4} }), ) }) ================================================ FILE: pkg/plugin/helpers.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "fmt" "path" "slices" "strings" "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/kubebuilder/v4/pkg/config" ) // KeyFor returns a Plugin's unique identifying string. func KeyFor(p Plugin) string { return path.Join(p.Name(), p.Version().String()) } // SplitKey returns a name and version for a plugin key. func SplitKey(key string) (string, string) { if !strings.Contains(key, "/") { return key, "" } keyParts := strings.SplitN(key, "/", 2) return keyParts[0], keyParts[1] } // GetPluginKeyForConfig finds which key to use when saving plugin config. // When a plugin is wrapped in a bundle, the bundle's key appears in the chain instead of the plugin's key. // For example: "deploy-image.my-domain/v1-alpha" wraps "deploy-image.go.kubebuilder.io/v1-alpha". // Returns the plugin's own key if nothing matches. func GetPluginKeyForConfig(pluginChain []string, p Plugin) string { pluginKey := KeyFor(p) // Try exact match first if slices.Contains(pluginChain, pluginKey) { return pluginKey } // No exact match. Try matching by base name + version to find bundled plugins. pluginName, _ := SplitKey(pluginKey) pluginVersion := p.Version().String() // Get base name (part before first dot): "deploy-image.go.kubebuilder.io" -> "deploy-image" baseName := pluginName if before, _, ok := strings.Cut(pluginName, "."); ok { baseName = before } for _, key := range pluginChain { name, version := SplitKey(key) if version != pluginVersion { continue } // Check if this key matches the base name keyBaseName := name if before, _, ok := strings.Cut(name, "."); ok { keyBaseName = before } if keyBaseName == baseName { return key } } // Nothing matched, use plugin's own key return pluginKey } // Validate ensures a Plugin is valid. func Validate(p Plugin) error { if err := validateName(p.Name()); err != nil { return fmt.Errorf("invalid plugin name %q: %w", p.Name(), err) } if err := p.Version().Validate(); err != nil { return fmt.Errorf("invalid plugin version %q: %w", p.Version(), err) } if len(p.SupportedProjectVersions()) == 0 { return fmt.Errorf("plugin %q must support at least one project version", KeyFor(p)) } for _, projectVersion := range p.SupportedProjectVersions() { if err := projectVersion.Validate(); err != nil { return fmt.Errorf("plugin %q supports an invalid project version %q: %w", KeyFor(p), projectVersion, err) } } return nil } // ValidateKey ensures both plugin name and version are valid. func ValidateKey(key string) error { name, version := SplitKey(key) if err := validateName(name); err != nil { return fmt.Errorf("invalid plugin name %q: %w", name, err) } // CLI-set plugins do not have to contain a version. if version != "" { var v Version if err := v.Parse(version); err != nil { return fmt.Errorf("invalid plugin version %q: %w", version, err) } } return nil } // validateName ensures name is a valid DNS 1123 subdomain. func validateName(name string) error { if errs := validation.IsDNS1123Subdomain(name); len(errs) != 0 { return fmt.Errorf("invalid plugin name %q: %v", name, errs) } return nil } // SupportsVersion checks if a plugin supports a project version. func SupportsVersion(p Plugin, projectVersion config.Version) bool { for _, version := range p.SupportedProjectVersions() { if projectVersion.Compare(version) == 0 { return true } } return false } // CommonSupportedProjectVersions returns the projects versions that are supported by all the provided Plugins func CommonSupportedProjectVersions(plugins ...Plugin) []config.Version { // Count how many times each supported project version appears supportedProjectVersionCounter := make(map[config.Version]int) for _, plugin := range plugins { for _, supportedProjectVersion := range plugin.SupportedProjectVersions() { if _, exists := supportedProjectVersionCounter[supportedProjectVersion]; !exists { supportedProjectVersionCounter[supportedProjectVersion] = 1 } else { supportedProjectVersionCounter[supportedProjectVersion]++ } } } // Check which versions are present the expected number of times supportedProjectVersions := make([]config.Version, 0, len(supportedProjectVersionCounter)) expectedTimes := len(plugins) for supportedProjectVersion, times := range supportedProjectVersionCounter { if times == expectedTimes { supportedProjectVersions = append(supportedProjectVersions, supportedProjectVersion) } } // Sort the output to guarantee consistency slices.SortStableFunc(supportedProjectVersions, func(a, b config.Version) int { return a.Compare(b) }) return supportedProjectVersions } ================================================ FILE: pkg/plugin/helpers_test.go ================================================ /* Copyright 2022 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 plugin import ( "slices" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" ) const ( short = "go" name = "go.kubebuilder.io" key = "go.kubebuilder.io/v1" ) var ( version = Version{Number: 1} supportedProjectVersions = []config.Version{ {Number: 2}, {Number: 3}, } ) var _ = Describe("KeyFor", func() { It("should join plugins name and version", func() { plugin := mockPlugin{ name: name, version: version, } Expect(KeyFor(plugin)).To(Equal(key)) }) }) var _ = Describe("SplitKey", func() { It("should split keys with versions", func() { n, v := SplitKey(key) Expect(n).To(Equal(name)) Expect(v).To(Equal(version.String())) }) It("should split keys without versions", func() { n, v := SplitKey(name) Expect(n).To(Equal(name)) Expect(v).To(Equal("")) }) }) var _ = Describe("Validate", func() { It("should succeed for valid plugins", func() { plugin := mockPlugin{ name: name, version: version, supportedProjectVersions: supportedProjectVersions, } Expect(Validate(plugin)).To(Succeed()) }) DescribeTable("should fail", func(plugin Plugin) { Expect(Validate(plugin)).NotTo(Succeed()) }, Entry("for invalid plugin names", mockPlugin{ name: "go_kubebuilder.io", version: version, supportedProjectVersions: supportedProjectVersions, }), Entry("for invalid plugin versions", mockPlugin{ name: name, version: Version{Number: -1}, supportedProjectVersions: supportedProjectVersions, }), Entry("for no supported project version", mockPlugin{ name: name, version: version, supportedProjectVersions: nil, }), Entry("for invalid supported project version", mockPlugin{ name: name, version: version, supportedProjectVersions: []config.Version{{Number: -1}}, }), ) }) var _ = Describe("ValidateKey", func() { It("should succeed for valid keys", func() { Expect(ValidateKey(key)).To(Succeed()) }) DescribeTable("should fail", func(key string) { Expect(ValidateKey(key)).NotTo(Succeed()) }, Entry("for invalid plugin names", "go_kubebuilder.io/v1"), Entry("for invalid versions", "go.kubebuilder.io/a"), ) }) var _ = Describe("SupportsVersion", func() { var plugin mockPlugin BeforeEach(func() { plugin = mockPlugin{ supportedProjectVersions: supportedProjectVersions, } }) It("should return true for supported versions", func() { Expect(SupportsVersion(plugin, config.Version{Number: 2})).To(BeTrue()) Expect(SupportsVersion(plugin, config.Version{Number: 3})).To(BeTrue()) }) It("should return false for non-supported versions", func() { Expect(SupportsVersion(plugin, config.Version{Number: 1})).To(BeFalse()) Expect(SupportsVersion(plugin, config.Version{Number: 3, Stage: stage.Alpha})).To(BeFalse()) }) }) var _ = Describe("CommonSupportedProjectVersions", func() { It("should return the common version", func() { var ( p1 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 1}, {Number: 2}, {Number: 3}, }} p2 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 1}, {Number: 2, Stage: stage.Beta}, {Number: 3, Stage: stage.Alpha}, }} p3 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 1}, {Number: 2}, {Number: 3, Stage: stage.Beta}, }} p4 = mockPlugin{supportedProjectVersions: []config.Version{ {Number: 2}, {Number: 3}, }} ) for _, tc := range []struct { plugins []Plugin versions []config.Version }{ {plugins: []Plugin{p1, p2}, versions: []config.Version{{Number: 1}}}, {plugins: []Plugin{p1, p3}, versions: []config.Version{{Number: 1}, {Number: 2}}}, {plugins: []Plugin{p1, p4}, versions: []config.Version{{Number: 2}, {Number: 3}}}, {plugins: []Plugin{p2, p3}, versions: []config.Version{{Number: 1}}}, {plugins: []Plugin{p2, p4}, versions: []config.Version{}}, {plugins: []Plugin{p3, p4}, versions: []config.Version{{Number: 2}}}, {plugins: []Plugin{p1, p2, p3}, versions: []config.Version{{Number: 1}}}, {plugins: []Plugin{p1, p2, p4}, versions: []config.Version{}}, {plugins: []Plugin{p1, p3, p4}, versions: []config.Version{{Number: 2}}}, {plugins: []Plugin{p2, p3, p4}, versions: []config.Version{}}, {plugins: []Plugin{p1, p2, p3, p4}, versions: []config.Version{}}, } { versions := CommonSupportedProjectVersions(tc.plugins...) slices.SortStableFunc(versions, func(a, b config.Version) int { return a.Compare(b) }) Expect(versions).To(Equal(tc.versions)) } }) }) var _ = Describe("GetPluginKeyForConfig", func() { It("should return the plugin's own key when it's in the plugin chain", func() { plugin := mockPlugin{ name: "deploy-image.go.kubebuilder.io", version: Version{Number: 1, Stage: stage.Alpha}, } pluginChain := []string{ "go.kubebuilder.io/v4", "deploy-image.go.kubebuilder.io/v1-alpha", } Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha")) }) It("should return the bundle key when plugin is wrapped in a bundle", func() { plugin := mockPlugin{ name: "deploy-image.go.kubebuilder.io", version: Version{Number: 1, Stage: stage.Alpha}, } pluginChain := []string{ "go.kubebuilder.io/v4", "deploy-image.my-domain/v1-alpha", } Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.my-domain/v1-alpha")) }) It("should fallback to plugin's own key when no match in chain", func() { plugin := mockPlugin{ name: "deploy-image.go.kubebuilder.io", version: Version{Number: 1, Stage: stage.Alpha}, } pluginChain := []string{ "go.kubebuilder.io/v4", } Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha")) }) It("should match on base name and version", func() { plugin := mockPlugin{ name: "deploy-image.go.kubebuilder.io", version: Version{Number: 1, Stage: stage.Alpha}, } pluginChain := []string{ "go.kubebuilder.io/v4", "deploy-image.operator-sdk.io/v1-alpha", } Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.operator-sdk.io/v1-alpha")) }) It("should not match if version differs", func() { plugin := mockPlugin{ name: "deploy-image.go.kubebuilder.io", version: Version{Number: 1, Stage: stage.Alpha}, } pluginChain := []string{ "go.kubebuilder.io/v4", "deploy-image.my-domain/v2-alpha", } Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha")) }) It("should not match if base name differs", func() { plugin := mockPlugin{ name: "deploy-image.go.kubebuilder.io", version: Version{Number: 1, Stage: stage.Alpha}, } pluginChain := []string{ "go.kubebuilder.io/v4", "other-plugin.my-domain/v1-alpha", } Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.go.kubebuilder.io/v1-alpha")) }) It("should choose the first matching bundle when multiple candidates exist", func() { plugin := mockPlugin{ name: "deploy-image.go.kubebuilder.io", version: Version{Number: 1, Stage: stage.Alpha}, } pluginChain := []string{ "deploy-image.first.example.com/v1-alpha", "deploy-image.second.example.com/v1-alpha", } Expect(GetPluginKeyForConfig(pluginChain, plugin)).To(Equal("deploy-image.first.example.com/v1-alpha")) }) }) ================================================ FILE: pkg/plugin/metadata.go ================================================ /* Copyright 2022 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 plugin // CLIMetadata is the runtime meta-data of the CLI type CLIMetadata struct { // CommandName is the root command name. CommandName string } // SubcommandMetadata is the runtime meta-data for a subcommand type SubcommandMetadata struct { // Description is a description of what this command does. It is used to display help. Description string // Examples are one or more examples of the command-line usage of this command. It is used to display help. Examples string } ================================================ FILE: pkg/plugin/plugin.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "sigs.k8s.io/kubebuilder/v4/pkg/config" ) // Plugin is an interface that defines the common base for all plugins. type Plugin interface { // Name returns a DNS1123 label string identifying the plugin uniquely. This name should be fully-qualified, // i.e. have a short prefix describing the plugin type (like a language) followed by a domain. // For example, Kubebuilder's main plugin would return "go.kubebuilder.io". Name() string // Version returns the plugin's version. // // NOTE: this version is different from config version. Version() Version // SupportedProjectVersions lists all project configuration versions this plugin supports. // The returned slice cannot be empty. SupportedProjectVersions() []config.Version } // Deprecated is an interface that defines the messages for plugins that are deprecated. type Deprecated interface { // DeprecationWarning returns a string indicating a plugin is deprecated. DeprecationWarning() string } // Describable is an optional interface for plugins that provide a short description. // This description is shown in help output to explain what the plugin does. type Describable interface { // Description returns a short description of the plugin (ideally one line). Description() string } // Init is an interface for plugins that provide an `init` subcommand. type Init interface { Plugin // GetInitSubcommand returns the underlying InitSubcommand interface. GetInitSubcommand() InitSubcommand } // CreateAPI is an interface for plugins that provide a `create api` subcommand. type CreateAPI interface { Plugin // GetCreateAPISubcommand returns the underlying CreateAPISubcommand interface. GetCreateAPISubcommand() CreateAPISubcommand } // CreateWebhook is an interface for plugins that provide a `create webhook` subcommand. type CreateWebhook interface { Plugin // GetCreateWebhookSubcommand returns the underlying CreateWebhookSubcommand interface. GetCreateWebhookSubcommand() CreateWebhookSubcommand } // Edit is an interface for plugins that provide a `edit` subcommand. type Edit interface { Plugin // GetEditSubcommand returns the underlying EditSubcommand interface. GetEditSubcommand() EditSubcommand } // Full is an interface for plugins that provide `init`, `create api`, `create webhook` and `edit` subcommands. type Full interface { Init CreateAPI CreateWebhook Edit } // Bundle allows to group plugins under a single key. type Bundle interface { Plugin // Plugins returns a list of the bundled plugins. // The returned list should be flattened, i.e., no plugin bundles should be part of this list. Plugins() []Plugin } ================================================ FILE: pkg/plugin/subcommand.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) // UpdatesMetadata is an interface that implements the optional metadata update method. type UpdatesMetadata interface { // UpdateMetadata updates the subcommand metadata. UpdateMetadata(CLIMetadata, *SubcommandMetadata) } // HasFlags is an interface that implements the optional bind flags method. type HasFlags interface { // BindFlags binds flags to the CLI subcommand. BindFlags(*pflag.FlagSet) } // RequiresConfig is an interface that implements the optional inject config method. type RequiresConfig interface { // InjectConfig injects the configuration to a subcommand. InjectConfig(config.Config) error } // RequiresResource is an interface that implements the required inject resource method. type RequiresResource interface { // InjectResource injects the resource model to a subcommand. InjectResource(*resource.Resource) error } // HasPreScaffold is an interface that implements the optional pre-scaffold method. type HasPreScaffold interface { // PreScaffold executes tasks before the main scaffolding. PreScaffold(machinery.Filesystem) error } // Scaffolder is an interface that implements the required scaffold method. type Scaffolder interface { // Scaffold implements the main scaffolding. Scaffold(machinery.Filesystem) error } // HasPostScaffold is an interface that implements the optional post-scaffold method. type HasPostScaffold interface { // PostScaffold executes tasks after the main scaffolding. PostScaffold() error } // Subcommand is a base interface for all subcommands. type Subcommand interface { Scaffolder } // InitSubcommand is an interface that represents an `init` subcommand. type InitSubcommand interface { Subcommand } // CreateAPISubcommand is an interface that represents a `create api` subcommand. type CreateAPISubcommand interface { Subcommand RequiresResource } // CreateWebhookSubcommand is an interface that represents a `create wekbhook` subcommand. type CreateWebhookSubcommand interface { Subcommand RequiresResource } // EditSubcommand is an interface that represents an `edit` subcommand. type EditSubcommand interface { Subcommand } ================================================ FILE: pkg/plugin/suite_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" ) func TestPlugin(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Plugin Suite") } type mockPlugin struct { name string version Version supportedProjectVersions []config.Version } func (p mockPlugin) Name() string { return p.name } func (p mockPlugin) Version() Version { return p.version } func (p mockPlugin) SupportedProjectVersions() []config.Version { return p.supportedProjectVersions } ================================================ FILE: pkg/plugin/util/exec.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package util import ( "fmt" log "log/slog" "os" "os/exec" ) // RunCmd prints the provided message and command and then executes it binding stdout and stderr func RunCmd(msg, cmd string, args ...string) error { c := exec.Command(cmd, args...) //nolint:gosec c.Stdout = os.Stdout c.Stderr = os.Stderr log.Info(msg) if err := c.Run(); err != nil { return fmt.Errorf("error running %q: %w", cmd, err) } return nil } ================================================ FILE: pkg/plugin/util/exec_test.go ================================================ /* Copyright 2022 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 util import ( "bytes" "os/exec" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("RunCmd", func() { var ( output *bytes.Buffer err error ) BeforeEach(func() { output = &bytes.Buffer{} }) AfterEach(func() { output.Reset() }) It("executes the command and redirects output to stdout", func() { cmd := exec.Command("echo", "test") cmd.Stdout = output err = cmd.Run() Expect(err).ToNot(HaveOccurred()) Expect(strings.TrimSpace(output.String())).To(Equal("test")) }) It("returns an error if the command fails", func() { err = RunCmd("unknown command", "unknowncommand") Expect(err).To(HaveOccurred()) }) }) ================================================ FILE: pkg/plugin/util/stdin.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 util import ( "bufio" "fmt" "log" "strings" ) // YesNo reads from stdin looking for one of "y", "yes", "n", "no" and returns // true for "y" and false for "n" func YesNo(reader *bufio.Reader) bool { for { text := readStdin(reader) switch text { case "y", "yes": return true case "n", "no": return false default: fmt.Printf("invalid input %q, should be [y/n]", text) } } } // readStdin reads a line from stdin trimming spaces, and returns the value. // log.Fatal's if there is an error. func readStdin(reader *bufio.Reader) string { text, err := reader.ReadString('\n') if err != nil { log.Fatalf("Error when reading input: %v", err) } return strings.TrimSpace(text) } ================================================ FILE: pkg/plugin/util/stdin_test.go ================================================ /* Copyright 2025 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 util import ( "bufio" "os" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("stdin", func() { It("returns true for 'y'", func() { reader := bufio.NewReader(strings.NewReader("y\n")) Expect(YesNo(reader)).To(BeTrue()) }) It("returns true for 'yes'", func() { reader := bufio.NewReader(strings.NewReader("yes\n")) Expect(YesNo(reader)).To(BeTrue()) }) It("returns false for 'n'", func() { reader := bufio.NewReader(strings.NewReader("n\n")) Expect(YesNo(reader)).To(BeFalse()) }) It("returns false for 'no'", func() { reader := bufio.NewReader(strings.NewReader("no\n")) Expect(YesNo(reader)).To(BeFalse()) }) It("prompts again on invalid input", func() { // "maybe" is invalid, then "y" is valid input := "maybe\ny\n" reader := bufio.NewReader(strings.NewReader(input)) // Capture stdout to check for prompt oldStdout := os.Stdout _, w, _ := os.Pipe() os.Stdout = w // Call YesNo directly (no goroutine needed) Expect(YesNo(reader)).To(BeTrue()) Expect(w.Close()).NotTo(HaveOccurred()) os.Stdout = oldStdout }) It("trims spaces and works", func() { reader := bufio.NewReader(strings.NewReader(" yes \n")) Expect(YesNo(reader)).To(BeTrue()) }) }) ================================================ FILE: pkg/plugin/util/suite_test.go ================================================ /* Copyright 2024 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 util import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestStage(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Utils Suite") } ================================================ FILE: pkg/plugin/util/util.go ================================================ /* Copyright 2019 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 util import ( "bufio" "bytes" "crypto/rand" "errors" "fmt" "math/big" "os" "regexp" "strings" ) const ( // KubebuilderBinName define the name of the kubebuilder binary to be used in the tests KubebuilderBinName = "kubebuilder" ) // RandomSuffix returns a 4-letter string. func RandomSuffix() (string, error) { source := []rune("abcdefghijklmnopqrstuvwxyz") res := make([]rune, 4) for i := range res { bi := new(big.Int) r, err := rand.Int(rand.Reader, bi.SetInt64(int64(len(source)))) if err != nil { return "", fmt.Errorf("failed to generate random number: %w", err) } res[i] = source[r.Int64()] } return string(res), nil } // GetNonEmptyLines converts given command output string into individual objects // according to line breakers, and ignores the empty elements in it. func GetNonEmptyLines(output string) []string { var res []string elements := strings.SplitSeq(output, "\n") for element := range elements { if element != "" { res = append(res, element) } } return res } // InsertCode searches target content in the file and insert `toInsert` after the target. func InsertCode(filename, target, code string) error { //nolint:gosec // false positive contents, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } idx := strings.Index(string(contents), target) if idx == -1 { return fmt.Errorf("string %s not found in %s", target, string(contents)) } out := string(contents[:idx+len(target)]) + code + string(contents[idx+len(target):]) //nolint:gosec // false positive if errWriteFile := os.WriteFile(filename, []byte(out), 0o644); errWriteFile != nil { return fmt.Errorf("failed to write file %q: %w", filename, errWriteFile) } return nil } // InsertCodeIfNotExist insert code if it does not already exist func InsertCodeIfNotExist(filename, target, code string) error { //nolint:gosec // false positive contents, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } idx := strings.Index(string(contents), code) if idx != -1 { return nil } return InsertCode(filename, target, code) } // AppendCodeIfNotExist checks if the code does not already exist in the file, and if not, appends it to the end. func AppendCodeIfNotExist(filename, code string) error { contents, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } if strings.Contains(string(contents), code) { return nil // Code already exists, no need to append. } return AppendCodeAtTheEnd(filename, code) } // AppendCodeAtTheEnd appends the given code at the end of the file. func AppendCodeAtTheEnd(filename, code string) error { f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return fmt.Errorf("failed to open file %q: %w", filename, err) } defer func() { if err = f.Close(); err != nil { return } }() if _, errWriteString := f.WriteString(code); errWriteString != nil { return fmt.Errorf("failed to write to file %q: %w", filename, errWriteString) } return nil } // UncommentCode searches for target in the file and remove the comment prefix // of the target content. The target content may span multiple lines. func UncommentCode(filename, target, prefix string) error { //nolint:gosec // false positive content, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } strContent := string(content) idx := strings.Index(strContent, target) if idx < 0 { return fmt.Errorf("unable to find the code %q to be uncommented", target) } out := new(bytes.Buffer) _, err = out.Write(content[:idx]) if err != nil { return fmt.Errorf("failed to write to file %q: %w", filename, err) } scanner := bufio.NewScanner(bytes.NewBufferString(target)) if !scanner.Scan() { return nil } for { if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil { return fmt.Errorf("failed to write to file %q: %w", filename, err) } // Avoid writing a newline in case the previous line was the last in target. if !scanner.Scan() { break } if _, err = out.WriteString("\n"); err != nil { return fmt.Errorf("failed to write to file %q: %w", filename, err) } } if _, err = out.Write(content[idx+len(target):]); err != nil { return fmt.Errorf("failed to write to file %q: %w", filename, err) } //nolint:gosec // false positive if err = os.WriteFile(filename, out.Bytes(), 0o644); err != nil { return fmt.Errorf("failed to write file %q: %w", filename, err) } return nil } // CommentCode searches for target in the file and adds the comment prefix // to the target content. The target content may span multiple lines. func CommentCode(filename, target, prefix string) error { // Read the file content content, err := os.ReadFile(filename) if err != nil { return fmt.Errorf("failed to read file %q: %w", filename, err) } strContent := string(content) // Find the target code to be commented idx := strings.Index(strContent, target) if idx < 0 { return fmt.Errorf("failed to find the code %q to be commented", target) } // Create a buffer to hold the modified content out := new(bytes.Buffer) if _, err = out.Write(content[:idx]); err != nil { return fmt.Errorf("failed to write to file %q: %w", filename, err) } // Add the comment prefix to each line of the target code scanner := bufio.NewScanner(bytes.NewBufferString(target)) for scanner.Scan() { if _, err = out.WriteString(prefix + scanner.Text() + "\n"); err != nil { return fmt.Errorf("failed to write to file %q: %w", filename, err) } } // Write the rest of the file content if _, err = out.Write(content[idx+len(target):]); err != nil { return fmt.Errorf("failed to write to file %q: %w", filename, err) } // Write the modified content back to the file if err = os.WriteFile(filename, out.Bytes(), 0o644); err != nil { return fmt.Errorf("failed to write file %q: %w", filename, err) } return nil } // EnsureExistAndReplace check if the content exists and then do the replacement func EnsureExistAndReplace(input, match, replace string) (string, error) { if !strings.Contains(input, match) { return "", fmt.Errorf("can't find %q", match) } return strings.ReplaceAll(input, match, replace), nil } // ReplaceInFile replaces all instances of old with new in the file at path. func ReplaceInFile(path, oldValue, newValue string) error { info, err := os.Stat(path) if err != nil { return fmt.Errorf("failed to stat file %q: %w", path, err) } //nolint:gosec // false positive b, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read file %q: %w", path, err) } if !strings.Contains(string(b), oldValue) { return errors.New("unable to find the content to be replaced") } s := strings.ReplaceAll(string(b), oldValue, newValue) if err = os.WriteFile(path, []byte(s), info.Mode()); err != nil { return fmt.Errorf("failed to write file %q: %w", path, err) } return nil } // ReplaceRegexInFile finds all strings that match `match` and replaces them // with `replace` in the file at path. // // This function is currently unused in the Kubebuilder codebase, // but is used by other projects and may be used in Kubebuilder in the future. func ReplaceRegexInFile(path, match, replace string) error { matcher, err := regexp.Compile(match) if err != nil { return fmt.Errorf("failed to compile regular expression %q: %w", match, err) } info, err := os.Stat(path) if err != nil { return fmt.Errorf("failed to stat file %q: %w", path, err) } //nolint:gosec // false positive b, err := os.ReadFile(path) if err != nil { return fmt.Errorf("failed to read file %q: %w", path, err) } s := matcher.ReplaceAllString(string(b), replace) if s == string(b) { return errors.New("unable to find the content to be replaced") } if err = os.WriteFile(path, []byte(s), info.Mode()); err != nil { return fmt.Errorf("failed to write file %q: %w", path, err) } return nil } // HasFileContentWith check if given `text` can be found in file func HasFileContentWith(path, text string) (bool, error) { //nolint:gosec contents, err := os.ReadFile(path) if err != nil { return false, fmt.Errorf("failed to read file %q: %w", path, err) } return strings.Contains(string(contents), text), nil } ================================================ FILE: pkg/plugin/util/util_test.go ================================================ /* Copyright 2022 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 util import ( "os" "path/filepath" "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) var _ = Describe("Cover plugin util helpers", func() { Describe("RandomSuffix", func() { It("should return a string with 4 caracteres", func() { suffix, err := RandomSuffix() Expect(err).NotTo(HaveOccurred()) Expect(suffix).To(HaveLen(4)) }) It("should return different values when call more than once", func() { suffix1, _ := RandomSuffix() suffix2, _ := RandomSuffix() Expect(suffix1).NotTo(Equal(suffix2)) }) }) Describe("GetNonEmptyLines", func() { It("should return non-empty lines", func() { output := "text1\n\ntext2\ntext3\n\n" lines := GetNonEmptyLines(output) Expect(lines).To(Equal([]string{"text1", "text2", "text3"})) }) It("should return an empty when an empty value is passed", func() { lines := GetNonEmptyLines("") Expect(lines).To(BeEmpty()) }) It("should return same string without empty lines", func() { output := "noemptylines" lines := GetNonEmptyLines(output) Expect(lines).To(Equal([]string{"noemptylines"})) }) }) Describe("InsertCode", Ordered, func() { var ( content []byte path string ) BeforeAll(func() { path = filepath.Join("testdata", "exampleFile.txt") err := os.MkdirAll("testdata", 0o755) Expect(err).NotTo(HaveOccurred()) if _, err = os.Stat(path); os.IsNotExist(err) { err = os.WriteFile(path, []byte("exampleTarget"), 0o644) Expect(err).NotTo(HaveOccurred()) } content, err = os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) }) AfterAll(func() { err := os.WriteFile(path, content, 0o644) Expect(err).NotTo(HaveOccurred()) err = os.RemoveAll("testdata") Expect(err).NotTo(HaveOccurred()) }) DescribeTable("should not succeed", func(target string) { Expect(InsertCode(path, target, "exampleCode")).ShouldNot(Succeed()) }, Entry("target given is not present in file", "randomTarget"), ) DescribeTable("should succeed", func(target string) { Expect(InsertCode(path, target, "exampleCode")).Should(Succeed()) }, Entry("target given is present in file", "exampleTarget"), ) }) Describe("InsertCodeIfNotExist", Ordered, func() { var ( content []byte path string ) BeforeAll(func() { path = filepath.Join("testdata", "exampleFile.txt") err := os.MkdirAll("testdata", 0o755) Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(path, []byte("target\n"), 0o644) Expect(err).NotTo(HaveOccurred()) content, err = os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) }) AfterAll(func() { err := os.WriteFile(path, content, 0o644) Expect(err).NotTo(HaveOccurred()) err = os.RemoveAll("testdata") Expect(err).NotTo(HaveOccurred()) }) It("should insert code if not present", func() { Expect(InsertCodeIfNotExist(path, "target", "code\n")).To(Succeed()) b, err := os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(ContainSubstring("code")) }) It("should not insert code if already present", func() { Expect(InsertCodeIfNotExist(path, "target", "code\n")).To(Succeed()) b, err := os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) // Only one "code" should be present Expect(strings.Count(string(b), "code")).To(Equal(1)) }) }) Describe("AppendCodeIfNotExist", Ordered, func() { var ( content []byte path string ) BeforeAll(func() { path = filepath.Join("testdata", "exampleFile.txt") err := os.MkdirAll("testdata", 0o755) Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(path, []byte("foo\n"), 0o644) Expect(err).NotTo(HaveOccurred()) content, err = os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) }) AfterAll(func() { err := os.WriteFile(path, content, 0o644) Expect(err).NotTo(HaveOccurred()) err = os.RemoveAll("testdata") Expect(err).NotTo(HaveOccurred()) }) It("should append code if not present", func() { Expect(AppendCodeIfNotExist(path, "code\n")).To(Succeed()) b, _ := os.ReadFile(path) Expect(string(b)).To(HaveSuffix("code\n")) }) It("should not append code if already present", func() { Expect(AppendCodeIfNotExist(path, "code\n")).To(Succeed()) b, _ := os.ReadFile(path) Expect(strings.Count(string(b), "code\n")).To(Equal(1)) }) }) Describe("UncommentCode and CommentCode", Ordered, func() { var ( content []byte path string ) BeforeAll(func() { path = filepath.Join("testdata", "exampleFile.txt") err := os.MkdirAll("testdata", 0o755) Expect(err).NotTo(HaveOccurred()) // Write a file with commented lines err = os.WriteFile(path, []byte("#line1\n#line2\nline3\n"), 0o644) Expect(err).NotTo(HaveOccurred()) content, err = os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) }) AfterAll(func() { err := os.WriteFile(path, content, 0o644) Expect(err).NotTo(HaveOccurred()) err = os.RemoveAll("testdata") Expect(err).NotTo(HaveOccurred()) }) It("should uncomment code with prefix", func() { target := "#line1\n#line2" Expect(UncommentCode(path, target, "#")).To(Succeed()) b, err := os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(ContainSubstring("line1\nline2\nline3\n")) Expect(string(b)).NotTo(ContainSubstring("#line1")) }) It("should comment code with prefix", func() { target := "line1\nline2\n" Expect(CommentCode(path, target, "#")).To(Succeed()) b, err := os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(ContainSubstring("#line1\n#line2\n")) }) It("should error if target not found for uncomment", func() { Expect(UncommentCode(path, "notfound", "#")).NotTo(Succeed()) }) It("should error if target not found for comment", func() { Expect(CommentCode(path, "notfound", "#")).NotTo(Succeed()) }) }) Describe("EnsureExistAndReplace", func() { Context("Content Exists", func() { It("should replace all the matched contents", func() { got, err := EnsureExistAndReplace("test", "t", "r") Expect(err).NotTo(HaveOccurred()) Expect(got).To(Equal("resr")) }) }) Context("Content Not Exists", func() { It("should error out", func() { got, err := EnsureExistAndReplace("test", "m", "r") Expect(err).To(HaveOccurred()) Expect(err.Error()).To(Equal(`can't find "m"`)) Expect(got).To(Equal("")) }) }) }) Describe("ReplaceInFile", Ordered, func() { var ( content []byte path string ) BeforeAll(func() { path = filepath.Join("testdata", "exampleFile.txt") err := os.MkdirAll("testdata", 0o755) Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(path, []byte("foo bar foo\nbaz foo\n"), 0o644) Expect(err).NotTo(HaveOccurred()) content, err = os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) }) AfterAll(func() { err := os.WriteFile(path, content, 0o644) Expect(err).NotTo(HaveOccurred()) err = os.RemoveAll("testdata") Expect(err).NotTo(HaveOccurred()) }) It("should replace all occurrences of a string", func() { Expect(ReplaceInFile(path, "foo", "qux")).To(Succeed()) b, err := os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal("qux bar qux\nbaz qux\n")) }) It("should error if oldValue not found", func() { Expect(ReplaceInFile(path, "notfound", "something")).NotTo(Succeed()) }) }) Describe("ReplaceRegexInFile", Ordered, func() { var ( content []byte path string ) BeforeAll(func() { path = filepath.Join("testdata", "exampleFile.txt") err := os.MkdirAll("testdata", 0o755) Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(path, []byte("foo123 bar456 foo789\nbaz000\n"), 0o644) Expect(err).NotTo(HaveOccurred()) content, err = os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) }) AfterAll(func() { err := os.WriteFile(path, content, 0o644) Expect(err).NotTo(HaveOccurred()) err = os.RemoveAll("testdata") Expect(err).NotTo(HaveOccurred()) }) It("should error if regex is invalid", func() { Expect(ReplaceRegexInFile(path, `\K`, "Z")).NotTo(Succeed()) }) It("should replace all regex matches", func() { Expect(ReplaceRegexInFile(path, `\d+`, "X")).To(Succeed()) b, err := os.ReadFile(path) Expect(err).NotTo(HaveOccurred()) Expect(string(b)).To(Equal("fooX barX fooX\nbazX\n")) }) It("should error if regex not found", func() { Expect(ReplaceRegexInFile(path, `notfound`, "Y")).NotTo(Succeed()) }) }) Describe("HasFileContentWith", Ordered, func() { const ( path = "testdata/PROJECT" content = `# Code generated by tool. DO NOT EDIT. # This file is used to track the info used to scaffold your project # and allow the plugins properly work. # More info: https://book.kubebuilder.io/reference/project-config.html domain: example.org layout: - go.kubebuilder.io/v4 - helm.kubebuilder.io/v1-alpha plugins: helm.kubebuilder.io/v1-alpha: {} repo: github.com/example/repo version: "3" ` ) BeforeAll(func() { err := os.MkdirAll("testdata", 0o755) Expect(err).NotTo(HaveOccurred()) if _, err = os.Stat(path); os.IsNotExist(err) { err = os.WriteFile(path, []byte(content), 0o644) Expect(err).NotTo(HaveOccurred()) } }) AfterAll(func() { err := os.RemoveAll("testdata") Expect(err).NotTo(HaveOccurred()) }) It("should return true when file contains the expected content", func() { content := "repo: github.com/example/repo" found, err := HasFileContentWith(path, content) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeTrue()) }) It("should return true when file contains multiline expected content", func() { content := `plugins: helm.kubebuilder.io/v1-alpha: {}` found, err := HasFileContentWith(path, content) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeTrue()) }) It("should return false when file does not contain the expected content", func() { content := "nonExistentContent" found, err := HasFileContentWith(path, content) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeFalse()) }) }) }) ================================================ FILE: pkg/plugin/version.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "errors" "fmt" "strconv" "strings" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" ) var ( errNegative = errors.New("plugin version number must be positive") errEmpty = errors.New("plugin version is empty") ) // Version is a plugin version containing a positive integer and a stage value that represents stability. type Version struct { // Number denotes the current version of a plugin. Two different numbers between versions // indicate that they are incompatible. Number int // Stage indicates stability. Stage stage.Stage } // Parse parses version inline, assuming it adheres to format: (v)?[0-9]*(-(alpha|beta))? func (v *Version) Parse(version string) error { version = strings.TrimPrefix(version, "v") if len(version) == 0 { return errEmpty } substrings := strings.SplitN(version, "-", 2) var err error if v.Number, err = strconv.Atoi(substrings[0]); err != nil { // Let's check if the `-` belonged to a negative number if n, errParse := strconv.Atoi(version); errParse == nil && n < 0 { return errNegative } return fmt.Errorf("error converting version number %q: %w", substrings[0], err) } if len(substrings) > 1 { if err = v.Stage.Parse(substrings[1]); err != nil { return fmt.Errorf("error parsing stage %q: %w", substrings[1], err) } } return nil } // String returns the string representation of v. func (v Version) String() string { stageStr := v.Stage.String() if len(stageStr) == 0 { return fmt.Sprintf("v%d", v.Number) } return fmt.Sprintf("v%d-%s", v.Number, stageStr) } // Validate ensures that the version number is positive and the stage is one of the valid stages. func (v Version) Validate() error { if v.Number < 0 { return errNegative } if err := v.Stage.Validate(); err != nil { return fmt.Errorf("error validating stage %q: %w", v.Stage, err) } return nil } // Compare returns -1 if v < other, 0 if v == other, and 1 if v > other. func (v Version) Compare(other Version) int { if v.Number > other.Number { return 1 } else if v.Number < other.Number { return -1 } return v.Stage.Compare(other.Stage) } // IsStable returns true if v is stable. func (v Version) IsStable() bool { // Plugin version 0 is not considered stable if v.Number == 0 { return false } // Any other version than 0 depends on its stage field return v.Stage.IsStable() } ================================================ FILE: pkg/plugin/version_test.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugin import ( "slices" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" ) var _ = Describe("Version", func() { Context("Parse", func() { DescribeTable("should be correctly parsed for valid version strings", func(str string, number int, s stage.Stage) { var v Version Expect(v.Parse(str)).To(Succeed()) Expect(v.Number).To(Equal(number)) Expect(v.Stage).To(Equal(s)) }, Entry("for version string `0`", "0", 0, stage.Stable), Entry("for version string `0-alpha`", "0-alpha", 0, stage.Alpha), Entry("for version string `0-beta`", "0-beta", 0, stage.Beta), Entry("for version string `1`", "1", 1, stage.Stable), Entry("for version string `1-alpha`", "1-alpha", 1, stage.Alpha), Entry("for version string `1-beta`", "1-beta", 1, stage.Beta), Entry("for version string `v1`", "v1", 1, stage.Stable), Entry("for version string `v1-alpha`", "v1-alpha", 1, stage.Alpha), Entry("for version string `v1-beta`", "v1-beta", 1, stage.Beta), Entry("for version string `22`", "22", 22, stage.Stable), Entry("for version string `22-alpha`", "22-alpha", 22, stage.Alpha), Entry("for version string `22-beta`", "22-beta", 22, stage.Beta), ) DescribeTable("should error when parsing an invalid version string", func(str string) { var v Version Expect(v.Parse(str)).NotTo(Succeed()) }, Entry("for version string ``", ""), Entry("for version string `-1`", "-1"), Entry("for version string `-1-alpha`", "-1-alpha"), Entry("for version string `-1-beta`", "-1-beta"), Entry("for version string `1.0`", "1.0"), Entry("for version string `v1.0`", "v1.0"), Entry("for version string `v1.0-alpha`", "v1.0-alpha"), Entry("for version string `1.0.0`", "1.0.0"), Entry("for version string `1-a`", "1-a"), ) }) Context("String", func() { DescribeTable("should return the correct string value", func(version Version, str string) { Expect(version.String()).To(Equal(str)) }, Entry("for version 0", Version{Number: 0}, "v0"), Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}, "v0"), Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}, "v0-alpha"), Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}, "v0-beta"), Entry("for version 0 (implicit)", Version{}, "v0"), Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}, "v0"), Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}, "v0-alpha"), Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}, "v0-beta"), Entry("for version 1", Version{Number: 1}, "v1"), Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}, "v1"), Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}, "v1-alpha"), Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}, "v1-beta"), Entry("for version 22", Version{Number: 22}, "v22"), Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}, "v22"), Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}, "v22-alpha"), Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}, "v22-beta"), ) }) Context("Validate", func() { DescribeTable("should validate valid versions", func(version Version) { Expect(version.Validate()).To(Succeed()) }, Entry("for version 0", Version{Number: 0}), Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}), Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}), Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}), Entry("for version 0 (implicit)", Version{}), Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}), Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}), Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}), Entry("for version 1", Version{Number: 1}), Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}), Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}), Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}), Entry("for version 22", Version{Number: 22}), Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}), Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}), Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), ) DescribeTable("should fail for invalid versions", func(version Version) { Expect(version.Validate()).NotTo(Succeed()) }, Entry("for version -1", Version{Number: -1}), Entry("for version -1 (stable)", Version{Number: -1, Stage: stage.Stable}), Entry("for version -1 (alpha)", Version{Number: -1, Stage: stage.Alpha}), Entry("for version -1 (beta)", Version{Number: -1, Stage: stage.Beta}), Entry("for invalid stage", Version{Stage: stage.Stage(34)}), ) }) Context("Compare", func() { // Test Compare() by sorting a list. var ( versions []Version sortedVersions []Version ) BeforeEach(func() { versions = []Version{ {Number: 2, Stage: stage.Alpha}, {Number: 44, Stage: stage.Alpha}, {Number: 1}, {Number: 2, Stage: stage.Beta}, {Number: 4, Stage: stage.Beta}, {Number: 1, Stage: stage.Alpha}, {Number: 4}, {Number: 44, Stage: stage.Alpha}, {Number: 30}, {Number: 4, Stage: stage.Alpha}, } sortedVersions = []Version{ {Number: 1, Stage: stage.Alpha}, {Number: 1}, {Number: 2, Stage: stage.Alpha}, {Number: 2, Stage: stage.Beta}, {Number: 4, Stage: stage.Alpha}, {Number: 4, Stage: stage.Beta}, {Number: 4}, {Number: 30}, {Number: 44, Stage: stage.Alpha}, {Number: 44, Stage: stage.Alpha}, } }) It("sorts a valid list of versions correctly", func() { slices.SortStableFunc(versions, func(a, b Version) int { return a.Compare(b) }) Expect(versions).To(Equal(sortedVersions)) }) }) Context("IsStable", func() { DescribeTable("should return true for stable versions", func(version Version) { Expect(version.IsStable()).To(BeTrue()) }, Entry("for version 1", Version{Number: 1}), Entry("for version 1 (stable)", Version{Number: 1, Stage: stage.Stable}), Entry("for version 22", Version{Number: 22}), Entry("for version 22 (stable)", Version{Number: 22, Stage: stage.Stable}), ) DescribeTable("should return false for unstable versions", func(version Version) { Expect(version.IsStable()).To(BeFalse()) }, Entry("for version 0", Version{Number: 0}), Entry("for version 0 (stable)", Version{Number: 0, Stage: stage.Stable}), Entry("for version 0 (alpha)", Version{Number: 0, Stage: stage.Alpha}), Entry("for version 0 (beta)", Version{Number: 0, Stage: stage.Beta}), Entry("for version 0 (implicit)", Version{}), Entry("for version 0 (stable) (implicit)", Version{Stage: stage.Stable}), Entry("for version 0 (alpha) (implicit)", Version{Stage: stage.Alpha}), Entry("for version 0 (beta) (implicit)", Version{Stage: stage.Beta}), Entry("for version 1 (alpha)", Version{Number: 1, Stage: stage.Alpha}), Entry("for version 1 (beta)", Version{Number: 1, Stage: stage.Beta}), Entry("for version 22 (alpha)", Version{Number: 22, Stage: stage.Alpha}), Entry("for version 22 (beta)", Version{Number: 22, Stage: stage.Beta}), ) }) }) ================================================ FILE: pkg/plugins/common/kustomize/v2/api.go ================================================ /* Copyright 2022 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 v2 import ( "fmt" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds" ) var _ plugin.CreateAPISubcommand = &createAPISubcommand{} type createAPISubcommand struct { createSubcommand } func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error { scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force) scaffolder.InjectFS(fs) if err := scaffolder.Scaffold(); err != nil { return fmt.Errorf("failed to scaffold api subcommand: %w", err) } return nil } ================================================ FILE: pkg/plugins/common/kustomize/v2/create.go ================================================ /* Copyright 2022 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 v2 import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) type createSubcommand struct { config config.Config resource *resource.Resource // force indicates whether to scaffold files even if they exist. force bool } func (p *createSubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.force, "force", false, "overwrite existing files") } func (p *createSubcommand) InjectConfig(c config.Config) error { p.config = c return nil } func (p *createSubcommand) InjectResource(res *resource.Resource) error { p.resource = res return nil } ================================================ FILE: pkg/plugins/common/kustomize/v2/create_test.go ================================================ /* Copyright 2026 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 v2 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" ) var _ = Describe("createSubcommand", func() { var ( subCmd *createSubcommand cfg config.Config res *resource.Resource ) BeforeEach(func() { subCmd = &createSubcommand{} cfg = cfgv3.New() res = &resource.Resource{ GVK: resource.GVK{ Group: "crew", Domain: "test.io", Version: "v1", Kind: "Captain", }, } }) It("should bind force flag and receive value via merge/sync", func() { flags := pflag.NewFlagSet("test", pflag.ContinueOnError) subCmd.BindFlags(flags) err := flags.Set("force", "true") Expect(err).NotTo(HaveOccurred()) Expect(subCmd.force).To(BeTrue()) }) It("should inject config and resource successfully", func() { Expect(subCmd.InjectConfig(cfg)).To(Succeed()) Expect(subCmd.config).To(Equal(cfg)) Expect(subCmd.InjectResource(res)).To(Succeed()) Expect(subCmd.resource).To(Equal(res)) }) }) ================================================ FILE: pkg/plugins/common/kustomize/v2/init.go ================================================ /* Copyright 2022 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 v2 import ( "fmt" "os" "path/filepath" "strings" "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/util/validation" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds" ) var _ plugin.InitSubcommand = &initSubcommand{} type initSubcommand struct { config config.Config // config options domain string name string } func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { subcmdMeta.Description = `Initialize a common project including the following files: - a "PROJECT" file that stores project configuration - several YAML files for project deployment under the "config" directory NOTE: This plugin requires kustomize version v5 and kubectl >= 1.22. ` subcmdMeta.Examples = fmt.Sprintf(` # Initialize a common project with your domain and name in copyright %[1]s init --plugins %[2]s --domain example.org # Initialize a common project defining a specific project version %[1]s init --plugins %[2]s --project-version 3 `, cliMeta.CommandName, plugin.KeyFor(Plugin{})) } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { fs.StringVar(&p.domain, "domain", "my.domain", "domain for groups") fs.StringVar(&p.name, "project-name", "", "name of this project") } func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c if err := p.config.SetDomain(p.domain); err != nil { return fmt.Errorf("error setting domain: %w", err) } // Assign a default project name if p.name == "" { dir, err := os.Getwd() if err != nil { return fmt.Errorf("error getting current directory: %w", err) } p.name = strings.ToLower(filepath.Base(dir)) } // Check if the project name is a valid k8s namespace (DNS 1123 label). if err := validation.IsDNS1123Label(p.name); err != nil { return fmt.Errorf("project name %q is invalid: %v", p.name, err) } if err := p.config.SetProjectName(p.name); err != nil { return fmt.Errorf("error setting project name: %w", err) } return nil } func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error { scaffolder := scaffolds.NewInitScaffolder(p.config) scaffolder.InjectFS(fs) if err := scaffolder.Scaffold(); err != nil { return fmt.Errorf("failed to scaffold init subcommand: %w", err) } return nil } ================================================ FILE: pkg/plugins/common/kustomize/v2/init_test.go ================================================ /* Copyright 2026 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 v2 import ( "os" "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" ) const testDomain = "example.com" var _ = Describe("initSubcommand", func() { var ( subCmd *initSubcommand cfg config.Config ) BeforeEach(func() { subCmd = &initSubcommand{} cfg = cfgv3.New() }) It("should set domain and project name", func() { subCmd.domain = testDomain subCmd.name = "my-project" err := subCmd.InjectConfig(cfg) Expect(err).NotTo(HaveOccurred()) Expect(cfg.GetDomain()).To(Equal(testDomain)) Expect(cfg.GetProjectName()).To(Equal("my-project")) }) It("should derive project name from directory when not provided", func() { originalDir, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.Chdir(originalDir) }() tmpDir, err := os.MkdirTemp("", "test-project-name") Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.RemoveAll(tmpDir) }() err = os.Chdir(tmpDir) Expect(err).NotTo(HaveOccurred()) subCmd.domain = testDomain subCmd.name = "" err = subCmd.InjectConfig(cfg) Expect(err).NotTo(HaveOccurred()) Expect(cfg.GetProjectName()).To(Equal(filepath.Base(tmpDir))) }) It("should reject invalid DNS 1123 label project names", func() { subCmd.domain = testDomain subCmd.name = "Invalid_Project" err := subCmd.InjectConfig(cfg) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("is invalid")) }) }) ================================================ FILE: pkg/plugins/common/kustomize/v2/plugin.go ================================================ /* Copyright 2022 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 v2 import ( "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugins" ) // KustomizeVersion is the kubernetes-sigs/kustomize version to be used in the project const KustomizeVersion = "v5.8.1" const pluginName = "kustomize.common." + plugins.DefaultNameQualifier var ( pluginVersion = plugin.Version{Number: 2, Stage: stage.Stable} supportedProjectVersions = []config.Version{cfgv3.Version} ) var ( _ plugin.Init = Plugin{} _ plugin.CreateAPI = Plugin{} _ plugin.CreateWebhook = Plugin{} ) // Plugin implements the plugin.Full interface type Plugin struct { initSubcommand createAPISubcommand createWebhookSubcommand } // Name returns the name of the plugin func (Plugin) Name() string { return pluginName } // Version returns the version of the plugin func (Plugin) Version() plugin.Version { return pluginVersion } // SupportedProjectVersions returns an array with all project versions supported by the plugin func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } // GetInitSubcommand will return the subcommand which is responsible for scaffolding init project func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand } // GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand } // GetCreateWebhookSubcommand will return the subcommand which is responsible for scaffolding webhooks func (p Plugin) GetCreateWebhookSubcommand() plugin.CreateWebhookSubcommand { return &p.createWebhookSubcommand } // Description returns a short description of the plugin func (Plugin) Description() string { return "Scaffolds base Kustomize configuration" } // DeprecationWarning define the deprecation message or return empty when plugin is not deprecated func (p Plugin) DeprecationWarning() string { return "" } ================================================ FILE: pkg/plugins/common/kustomize/v2/plugin_test.go ================================================ /* Copyright 2026 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 v2 import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" ) var _ = Describe("Plugin", func() { var p Plugin It("should have correct version and support v3 projects", func() { Expect(p.Version().Number).To(Equal(2)) Expect(p.SupportedProjectVersions()).To(ContainElement(cfgv3.Version)) }) It("should not be deprecated", func() { Expect(p.DeprecationWarning()).To(BeEmpty()) }) }) ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/api.go ================================================ /* Copyright 2022 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 scaffolds import ( "fmt" log "log/slog" "strings" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/pkg/plugins" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/samples" ) var _ plugins.Scaffolder = &apiScaffolder{} // apiScaffolder contains configuration for generating scaffolding for Go type // representing the API and controller that implements the behavior for the API. type apiScaffolder struct { config config.Config resource resource.Resource // fs is the filesystem that will be used by the scaffolder fs machinery.Filesystem // force indicates whether to scaffold files even if they exist. force bool } // NewAPIScaffolder returns a new Scaffolder for API/controller creation operations func NewAPIScaffolder(cfg config.Config, res resource.Resource, force bool) plugins.Scaffolder { return &apiScaffolder{ config: cfg, resource: res, force: force, } } // InjectFS implements cmdutil.Scaffolder func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) { s.fs = fs } // Scaffold implements cmdutil.Scaffolder func (s *apiScaffolder) Scaffold() error { log.Info("Writing kustomize manifests for you to edit...") // Initialize the machinery.Scaffold that will write the files to disk scaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config), machinery.WithResource(&s.resource), ) // Keep track of these values before the update if s.resource.HasAPI() { if err := scaffold.Execute( &samples.CRDSample{Force: s.force}, &rbac.CRDAdminRole{}, &rbac.CRDEditorRole{}, &rbac.CRDViewerRole{}, &crd.Kustomization{}, &crd.KustomizeConfig{}, ); err != nil { return fmt.Errorf("error scaffolding kustomize API manifests: %w", err) } // If the gvk is non-empty if s.resource.Group != "" || s.resource.Version != "" || s.resource.Kind != "" { if err := scaffold.Execute(&samples.Kustomization{}); err != nil { return fmt.Errorf("error scaffolding manifests: %w", err) } } err := pluginutil.UncommentCode(kustomizeFilePath, "#- ../crd", `#`) if err != nil { hasCRUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, "- ../crd") if !hasCRUncommented || errCheck != nil { log.Error("unable to find the target #- ../crd to uncomment in the file", "file_path", kustomizeFilePath) } } comment := fmt.Sprintf(adminEditViewRulesCommentFragment, s.config.GetProjectName()) // Add scaffolded CRD Admin, Editor and Viewer roles in config/rbac/kustomization.yaml rbacKustomizeFilePath := "config/rbac/kustomization.yaml" err = pluginutil.AppendCodeIfNotExist(rbacKustomizeFilePath, comment) if err != nil { log.Error("failed to append the admin/edit/view roles comment in the file", "file_path", rbacKustomizeFilePath) } crdName := strings.ToLower(s.resource.Kind) if s.config.IsMultiGroup() && s.resource.Group != "" { crdName = strings.ToLower(s.resource.Group) + "_" + crdName } err = pluginutil.InsertCodeIfNotExist(rbacKustomizeFilePath, comment, fmt.Sprintf("\n- %[1]s_admin_role.yaml\n- %[1]s_editor_role.yaml\n- %[1]s_viewer_role.yaml", crdName)) if err != nil { log.Error("failed to add admin, editor and viewer roles in the file", "file_path", rbacKustomizeFilePath) } // Add an empty line at the end of the file err = pluginutil.AppendCodeIfNotExist(rbacKustomizeFilePath, ` `) if err != nil { log.Error("failed to append empty line at the end of the file", "file_path", rbacKustomizeFilePath) } } return nil } const adminEditViewRulesCommentFragment = `# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by # default, aiding admins in cluster management. Those roles are # not used by the %s itself. You can comment the following lines # if you do not want those helpers be installed with your Project.` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/edit.go ================================================ /* Copyright 2026 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 scaffolds import ( "fmt" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugins" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/manager" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac" ) var _ plugins.Scaffolder = &editScaffolder{} type editScaffolder struct { config config.Config namespaced bool force bool // fs is the filesystem that will be used by the scaffolder fs machinery.Filesystem } // NewEditScaffolder returns a new Scaffolder for configuration edit operations func NewEditScaffolder(cfg config.Config, namespaced bool, force bool) plugins.Scaffolder { return &editScaffolder{ config: cfg, namespaced: namespaced, force: force, } } // InjectFS implements plugins.Scaffolder func (s *editScaffolder) InjectFS(fs machinery.Filesystem) { s.fs = fs } // Scaffold implements plugins.Scaffolder func (s *editScaffolder) Scaffold() error { // Initialize the machinery.Scaffold scaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config), ) var templates []machinery.Builder if s.namespaced { // Scaffold namespace-scoped RBAC and manager config templates = []machinery.Builder{ &rbac.NamespacedRole{}, &rbac.NamespacedRoleBinding{}, &manager.Config{Image: imageName, Force: s.force}, } } else { // Scaffold cluster-scoped RBAC and manager config templates = []machinery.Builder{ &rbac.ClusterRole{}, &rbac.ClusterRoleBinding{}, &manager.Config{Image: imageName, Force: s.force}, } } if err := scaffold.Execute(templates...); err != nil { return fmt.Errorf("failed to scaffold: %w", err) } // Regenerate CRD admin/editor/viewer roles for all existing resources // to match the new namespaced/cluster-scoped configuration resources, err := s.config.GetResources() if err != nil { return fmt.Errorf("failed to get resources: %w", err) } for _, res := range resources { if res.HasAPI() { // Create a scaffold with the resource injected for each resource resourceScaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config), machinery.WithResource(&res), ) if err := resourceScaffold.Execute( &rbac.CRDAdminRole{}, &rbac.CRDEditorRole{}, &rbac.CRDViewerRole{}, ); err != nil { return fmt.Errorf("failed to scaffold CRD roles for %s: %w", res.Kind, err) } } } return nil } ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/init.go ================================================ /* Copyright 2022 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 scaffolds import ( "fmt" log "log/slog" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugins" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/manager" networkpolicy "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/network-policy" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/prometheus" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac" ) const ( imageName = "controller:latest" ) var _ plugins.Scaffolder = &initScaffolder{} type initScaffolder struct { config config.Config // fs is the filesystem that will be used by the scaffolder fs machinery.Filesystem } // NewInitScaffolder returns a new Scaffolder for project initialization operations func NewInitScaffolder(cfg config.Config) plugins.Scaffolder { return &initScaffolder{ config: cfg, } } // InjectFS implements cmdutil.Scaffolder func (s *initScaffolder) InjectFS(fs machinery.Filesystem) { s.fs = fs } // Scaffold implements cmdutil.Scaffolder func (s *initScaffolder) Scaffold() error { log.Info("Writing kustomize manifests for you to edit...") // Initialize the machinery.Scaffold that will write the files to disk scaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config), ) templates := []machinery.Builder{ &rbac.Kustomization{}, &kdefault.MetricsService{}, &rbac.MetricsAuthRole{}, &rbac.MetricsAuthRoleBinding{}, &rbac.MetricsReaderRole{}, &rbac.LeaderElectionRole{}, &rbac.LeaderElectionRoleBinding{}, &rbac.ServiceAccount{}, &manager.Kustomization{}, &kdefault.ManagerMetricsPatch{}, &kdefault.CertManagerMetricsPatch{}, &manager.Config{Image: imageName}, &kdefault.Kustomization{}, &networkpolicy.Kustomization{}, &networkpolicy.PolicyAllowMetrics{}, &prometheus.Kustomization{}, &prometheus.Monitor{}, &prometheus.ServiceMonitorPatch{}, } // Scaffold appropriate RBAC based on scope // We need to create a Role/ClusterRole because if the project // has no CRDs defined, controller-gen will not generate this file if s.config.IsNamespaced() { templates = append(templates, &rbac.NamespacedRoleBinding{}, &rbac.NamespacedRole{}, ) } else { templates = append(templates, &rbac.ClusterRoleBinding{}, &rbac.ClusterRole{}, ) } if err := scaffold.Execute(templates...); err != nil { return fmt.Errorf("failed to scaffold kustomize manifests: %w", err) } return nil } ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/certmanager/certificate_metrics.go ================================================ /* Copyright 2024 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 certmanager import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &MetricsCertificate{} // MetricsCertificate scaffolds a file that defines the issuer CR and the metrics certificate CR type MetricsCertificate struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *MetricsCertificate) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "certmanager", "certificate-metrics.yaml") } f.TemplateBody = metricsCertManagerTemplate // If file exists, skip creation. f.IfExistsAction = machinery.SkipFile return nil } //nolint:lll const metricsCertManagerTemplate = `# The following manifests contain a self-signed issuer CR and a metrics certificate CR. # More document can be found at https://docs.cert-manager.io apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: metrics-certs # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: dnsNames: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize # replacements in the config/default/kustomization.yaml file. - SERVICE_NAME.SERVICE_NAMESPACE.svc - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer secretName: metrics-server-cert ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/certmanager/certificate_webhook.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package certmanager import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Certificate{} // Certificate scaffolds a file that defines the issuer CR and the certificate CR type Certificate struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *Certificate) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "certmanager", "certificate-webhook.yaml") } f.TemplateBody = certManagerTemplate // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = machinery.SkipFile return nil } const certManagerTemplate = `# The following manifests contain a self-signed issuer CR and a certificate CR. # More document can be found at https://docs.cert-manager.io apiVersion: cert-manager.io/v1 kind: Certificate metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml namespace: system spec: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize # replacements in the config/default/kustomization.yaml file. dnsNames: - SERVICE_NAME.SERVICE_NAMESPACE.svc - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local issuerRef: kind: Issuer name: selfsigned-issuer secretName: webhook-server-cert ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/certmanager/issuer.go ================================================ /* Copyright 2024 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 certmanager import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Issuer{} // Issuer scaffolds a file that defines the self-signed Issuer CR type Issuer struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *Issuer) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "certmanager", "issuer.yaml") } f.TemplateBody = issuerTemplate // If file exists, skip creation. f.IfExistsAction = machinery.SkipFile return nil } const issuerTemplate = `# The following manifest contains a self-signed issuer CR. # More information can be found at https://docs.cert-manager.io # WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes. apiVersion: cert-manager.io/v1 kind: Issuer metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: selfsigned-issuer namespace: system spec: selfSigned: {} ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/certmanager/kustomization.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package certmanager import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the certmanager folder type Kustomization struct { machinery.TemplateMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "certmanager", "kustomization.yaml") } f.TemplateBody = kustomizationTemplate // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = machinery.SkipFile return nil } const kustomizationTemplate = `resources: - issuer.yaml - certificate-webhook.yaml - certificate-metrics.yaml configurations: - kustomizeconfig.yaml ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/certmanager/kustomizeconfig.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package certmanager import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the certmanager folder type KustomizeConfig struct { machinery.TemplateMixin } // SetTemplateDefaults implements machinery.Template func (f *KustomizeConfig) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "certmanager", "kustomizeconfig.yaml") } f.TemplateBody = kustomizeConfigTemplate // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = machinery.SkipFile return nil } const kustomizeConfigTemplate = `# This configuration is for teaching kustomize how to update name ref substitution nameReference: - kind: Issuer group: cert-manager.io fieldSpecs: - kind: Certificate group: cert-manager.io path: spec/issuerRef/name ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd/kustomization.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package crd import ( "fmt" "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var ( _ machinery.Template = &Kustomization{} _ machinery.Inserter = &Kustomization{} ) // Kustomization scaffolds a file that defines the kustomization scheme for the crd folder type Kustomization struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.ResourceMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "crd", "kustomization.yaml") } f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = fmt.Sprintf(kustomizationTemplate, machinery.NewMarkerFor(f.Path, resourceMarker), machinery.NewMarkerFor(f.Path, webhookPatchMarker), ) return nil } //nolint:gosec // to ignore false complain G101: Potential hardcoded credentials (gosec) const ( resourceMarker = "crdkustomizeresource" webhookPatchMarker = "crdkustomizewebhookpatch" ) // GetMarkers implements file.Inserter func (f *Kustomization) GetMarkers() []machinery.Marker { return []machinery.Marker{ machinery.NewMarkerFor(f.Path, resourceMarker), machinery.NewMarkerFor(f.Path, webhookPatchMarker), } } const ( resourceCodeFragment = `- bases/%s_%s.yaml ` webhookPatchCodeFragment = `- path: patches/webhook_in_%s.yaml ` ) // GetCodeFragments implements file.Inserter func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap { fragments := make(machinery.CodeFragmentsMap, 2) // Generate resource code fragments res := make([]string, 0, 1) res = append(res, fmt.Sprintf(resourceCodeFragment, f.Resource.QualifiedGroup(), f.Resource.Plural)) suffix := f.Resource.Plural if f.MultiGroup && f.Resource.Group != "" { suffix = f.Resource.Group + "_" + f.Resource.Plural } if !f.Resource.Webhooks.IsEmpty() && f.Resource.Webhooks.Conversion { webhookPatch := fmt.Sprintf(webhookPatchCodeFragment, suffix) marker := machinery.NewMarkerFor(f.Path, webhookPatchMarker) if _, exists := fragments[marker]; !exists { fragments[marker] = []string{webhookPatch} } } // Only store code fragments in the map if the slices are non-empty if len(res) != 0 { fragments[machinery.NewMarkerFor(f.Path, resourceMarker)] = res } return fragments } var kustomizationTemplate = `# This kustomization.yaml is not intended to be run by itself, # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: %s patches: # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. # patches here are for enabling the conversion webhook for each CRD %s # [WEBHOOK] To enable webhook, uncomment the following section # the following config is for teaching kustomize how to do kustomization for CRDs. #configurations: #- kustomizeconfig.yaml ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd/kustomizeconfig.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package crd import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &KustomizeConfig{} // KustomizeConfig scaffolds a file that configures the kustomization for the crd folder type KustomizeConfig struct { machinery.TemplateMixin machinery.ResourceMixin } // SetTemplateDefaults implements machinery.Template func (f *KustomizeConfig) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "crd", "kustomizeconfig.yaml") } f.TemplateBody = kustomizeConfigTemplate return nil } //nolint:lll const kustomizeConfigTemplate = `# This file is for teaching kustomize how to substitute name and namespace reference in CRD nameReference: - kind: Service version: v1 fieldSpecs: - kind: CustomResourceDefinition version: v1 group: apiextensions.k8s.io path: spec/conversion/webhook/clientConfig/service/name varReference: - path: metadata/annotations ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd/patches/enablecainjection_patch.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package patches import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &EnableCAInjectionPatch{} // EnableCAInjectionPatch scaffolds a file that defines the patch that injects CA into the CRD type EnableCAInjectionPatch struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.ResourceMixin } // SetTemplateDefaults implements machinery.Template func (f *EnableCAInjectionPatch) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("config", "crd", "patches", "cainjection_in_%[group]_%[plural].yaml") } else { f.Path = filepath.Join("config", "crd", "patches", "cainjection_in_%[plural].yaml") } } f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = enableCAInjectionPatchTemplate return nil } const enableCAInjectionPatchTemplate = `# The following patch adds a directive for certmanager to inject CA into the CRD apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }} ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd/patches/enablewebhook_patch.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package patches import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &EnableWebhookPatch{} // EnableWebhookPatch scaffolds a file that defines the patch that enables conversion webhook for the CRD type EnableWebhookPatch struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.ResourceMixin } // SetTemplateDefaults implements machinery.Template func (f *EnableWebhookPatch) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("config", "crd", "patches", "webhook_in_%[group]_%[plural].yaml") } else { f.Path = filepath.Join("config", "crd", "patches", "webhook_in_%[plural].yaml") } } f.Path = f.Resource.Replacer().Replace(f.Path) f.TemplateBody = enableWebhookPatchTemplate return nil } const enableWebhookPatchTemplate = `# The following patch enables a conversion webhook for the CRD apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: name: {{ .Resource.Plural }}.{{ .Resource.QualifiedGroup }} spec: conversion: strategy: Webhook webhook: clientConfig: service: namespace: system name: webhook-service path: /convert conversionReviewVersions: - v1 ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault/cert_metrics_manager_patch.go ================================================ /* Copyright 2024 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 kdefault import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &CertManagerMetricsPatch{} // CertManagerMetricsPatch scaffolds a file that defines the patch that enables webhooks on the manager type CertManagerMetricsPatch struct { machinery.TemplateMixin machinery.ProjectNameMixin Force bool } // SetTemplateDefaults implements machinery.Template func (f *CertManagerMetricsPatch) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "default", "cert_metrics_manager_patch.yaml") } f.TemplateBody = metricsManagerPatchTemplate if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = machinery.SkipFile } return nil } //nolint:lll const metricsManagerPatchTemplate = `# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs. # Add the volumeMount for the metrics-server certs - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-metrics-server/metrics-certs name: metrics-certs readOnly: true # Add the --metrics-cert-path argument for the metrics server - op: add path: /spec/template/spec/containers/0/args/- value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs # Add the metrics-server certs volume configuration - op: add path: /spec/template/spec/volumes/- value: name: metrics-certs secret: secretName: metrics-server-cert optional: false items: - key: ca.crt path: ca.crt - key: tls.crt path: tls.crt - key: tls.key path: tls.key ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault/kustomization.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kdefault import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the default overlay folder type Kustomization struct { machinery.TemplateMixin machinery.ProjectNameMixin machinery.NamespacedMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "default", "kustomization.yaml") } f.TemplateBody = kustomizeTemplate f.IfExistsAction = machinery.Error return nil } const kustomizeTemplate = `# Adds namespace to all resources. namespace: {{ .ProjectName }}-system # Value of this field is prepended to the # names of all resources, e.g. a deployment named # "wordpress" becomes "alices-wordpress". # Note that it should also match with the prefix (text before '-') of the namespace # field above. namePrefix: {{ .ProjectName }}- # Labels to add to all resources and selectors. #labels: #- includeSelectors: true # pairs: # someName: someValue resources: #- ../crd - ../rbac - ../manager # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- ../webhook # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. #- ../certmanager # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. #- ../prometheus # [METRICS] Expose the controller manager metrics service. - metrics_service.yaml # [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy. # Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics. # Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will # be able to communicate with the Webhook Server. #- ../network-policy # Uncomment the patches line if you enable Metrics patches: # [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443. # More info: https://book.kubebuilder.io/reference/metrics - path: manager_metrics_patch.yaml target: kind: Deployment # Uncomment the patches line if you enable Metrics and CertManager # [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line. # This patch will protect the metrics with certManager self-signed certs. #- path: cert_metrics_manager_patch.yaml # target: # kind: Deployment # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in # crd/kustomization.yaml #- path: manager_webhook_patch.yaml # target: # kind: Deployment # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. # Uncomment the following replacements to add the cert-manager CA injection annotations #replacements: # - source: # Uncomment the following block to enable certificates for metrics # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.name # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 0 # create: true # - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor # kind: ServiceMonitor # group: monitoring.coreos.com # version: v1 # name: controller-manager-metrics-monitor # fieldPaths: # - spec.endpoints.0.tlsConfig.serverName # options: # delimiter: '.' # index: 0 # create: true # - source: # kind: Service # version: v1 # name: controller-manager-metrics-service # fieldPath: metadata.namespace # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: metrics-certs # fieldPaths: # - spec.dnsNames.0 # - spec.dnsNames.1 # options: # delimiter: '.' # index: 1 # create: true # - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor # kind: ServiceMonitor # group: monitoring.coreos.com # version: v1 # name: controller-manager-metrics-monitor # fieldPaths: # - spec.endpoints.0.tlsConfig.serverName # options: # delimiter: '.' # index: 1 # create: true # - source: # Uncomment the following block if you have any webhook # kind: Service # version: v1 # name: webhook-service # fieldPath: .metadata.name # Name of the service # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPaths: # - .spec.dnsNames.0 # - .spec.dnsNames.1 # options: # delimiter: '.' # index: 0 # create: true # - source: # kind: Service # version: v1 # name: webhook-service # fieldPath: .metadata.namespace # Namespace of the service # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPaths: # - .spec.dnsNames.0 # - .spec.dnsNames.1 # options: # delimiter: '.' # index: 1 # create: true # - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # This name should match the one in certificate.yaml # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # - select: # kind: ValidatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # - select: # kind: ValidatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true # - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # - select: # kind: MutatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # - select: # kind: MutatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectionns # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectionname ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault/kustomization_conversion_updater.go ================================================ /* Copyright 2024 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 kdefault import ( "fmt" "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) const ( caNamespace = "crdkustomizecainjectionns" caName = "crdkustomizecainjectionname" ) // KustomizationCAConversionUpdater appends CA injection targets for CRDs with --conversion type KustomizationCAConversionUpdater struct { machinery.TemplateMixin machinery.ResourceMixin } // SetTemplateDefaults defines the file path and behavior for existing files func (f *KustomizationCAConversionUpdater) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "default", "kustomization.yaml") } f.IfExistsAction = machinery.SkipFile // Only append to the existing file, don’t overwrite it return nil } // GetMarkers provides the markers where the CA injection targets will be appended func (f *KustomizationCAConversionUpdater) GetMarkers() []machinery.Marker { return []machinery.Marker{ machinery.NewMarkerFor(f.Path, caNamespace), machinery.NewMarkerFor(f.Path, caName), } } // GetCodeFragments appends CA injection targets for the CRD with --conversion as comments func (f *KustomizationCAConversionUpdater) GetCodeFragments() machinery.CodeFragmentsMap { fragments := make(machinery.CodeFragmentsMap) // Obtain the formatted CRD name as Plural.Group.Domain (e.g., cronjobs.batch.tutorial.kubebuilder.io) crdName := fmt.Sprintf("%s.%s", f.Resource.Plural, f.Resource.QualifiedGroup()) if !f.Resource.Webhooks.IsEmpty() && f.Resource.Webhooks.Conversion { // Commented CA injection configuration for the namespace part caInjectionNamespace := fmt.Sprintf(`# - select: # kind: CustomResourceDefinition # name: %s # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true `, crdName) // Commented CA injection configuration for the name part caInjectionName := fmt.Sprintf(`# - select: # kind: CustomResourceDefinition # name: %s # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true `, crdName) // Append to the correct markers to prevent duplication namespaceMarker := machinery.NewMarkerFor(f.Path, caNamespace) certificateMarker := machinery.NewMarkerFor(f.Path, caName) // Check if the fragments already exist before adding them if _, exists := fragments[namespaceMarker]; !exists { fragments[namespaceMarker] = []string{caInjectionNamespace} } if _, exists := fragments[certificateMarker]; !exists { fragments[certificateMarker] = []string{caInjectionName} } } return fragments } ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault/manager_metrics_patch.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kdefault import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &ManagerMetricsPatch{} // ManagerMetricsPatch scaffolds a file that defines the patch that enables prometheus metrics for the manager type ManagerMetricsPatch struct { machinery.TemplateMixin } // SetTemplateDefaults implements machinery.Template func (f *ManagerMetricsPatch) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "default", "manager_metrics_patch.yaml") } f.TemplateBody = kustomizeMetricsPatchTemplate f.IfExistsAction = machinery.Error return nil } const kustomizeMetricsPatchTemplate = `# This patch adds the args to allow exposing the metrics endpoint using HTTPS - op: add path: /spec/template/spec/containers/0/args/0 value: --metrics-bind-address=:8443 ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault/metrics_service.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kdefault import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &MetricsService{} // MetricsService scaffolds a file that defines the service for the auth proxy type MetricsService struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *MetricsService) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "default", "metrics_service.yaml") } f.TemplateBody = metricsServiceTemplate return nil } const metricsServiceTemplate = `apiVersion: v1 kind: Service metadata: labels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-service namespace: system spec: ports: - name: https port: 8443 protocol: TCP targetPort: 8443 selector: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault/webhook_manager_patch.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package kdefault import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &ManagerWebhookPatch{} // ManagerWebhookPatch scaffolds a file that defines the patch that enables webhooks on the manager type ManagerWebhookPatch struct { machinery.TemplateMixin machinery.ProjectNameMixin Force bool } // SetTemplateDefaults implements machinery.Template func (f *ManagerWebhookPatch) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "default", "manager_webhook_patch.yaml") } f.TemplateBody = managerWebhookPatchTemplate if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = machinery.SkipFile } return nil } //nolint:lll const managerWebhookPatchTemplate = `# This patch ensures the webhook certificates are properly mounted in the manager container. # It configures the necessary arguments, volumes, volume mounts, and container ports. # Add the --webhook-cert-path argument for configuring the webhook certificate path - op: add path: /spec/template/spec/containers/0/args/- value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs # Add the volumeMount for the webhook certificates - op: add path: /spec/template/spec/containers/0/volumeMounts/- value: mountPath: /tmp/k8s-webhook-server/serving-certs name: webhook-certs readOnly: true # Add the port configuration for the webhook server - op: add path: /spec/template/spec/containers/0/ports/- value: containerPort: 9443 name: webhook-server protocol: TCP # Add the volume configuration for the webhook certificates - op: add path: /spec/template/spec/volumes/- value: name: webhook-certs secret: secretName: webhook-server-cert ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/manager/config.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manager import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Config{} // Config scaffolds a file that defines the namespace and the manager deployment type Config struct { machinery.TemplateMixin machinery.ProjectNameMixin machinery.NamespacedMixin // Image is controller manager image name Image string // Force if true allows overwriting the scaffolded file Force bool } // SetTemplateDefaults implements machinery.Template func (f *Config) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "manager", "manager.yaml") } f.TemplateBody = configTemplate if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { f.IfExistsAction = machinery.SkipFile } return nil } const configTemplate = `apiVersion: v1 kind: Namespace metadata: labels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: system --- apiVersion: apps/v1 kind: Deployment metadata: name: controller-manager namespace: system labels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize spec: selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} replicas: 1 template: metadata: annotations: kubectl.kubernetes.io/default-container: manager labels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} spec: # TODO(user): Uncomment the following code to configure the nodeAffinity expression # according to the platforms which are supported by your solution. # It is considered best practice to support multiple architectures. You can # build your manager image using the makefile target docker-buildx. # affinity: # nodeAffinity: # requiredDuringSchedulingIgnoredDuringExecution: # nodeSelectorTerms: # - matchExpressions: # - key: kubernetes.io/arch # operator: In # values: # - amd64 # - arm64 # - ppc64le # - s390x # - key: kubernetes.io/os # operator: In # values: # - linux securityContext: # Projects are configured by default to adhere to the "restricted" Pod Security Standards. # This ensures that deployments meet the highest security requirements for Kubernetes. # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - command: - /manager args: - --leader-elect - --health-probe-bind-address=:8081 image: {{ .Image }} name: manager {{- if .Namespaced }} env: - name: WATCH_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace {{- end }} ports: [] securityContext: readOnlyRootFilesystem: true allowPrivilegeEscalation: false capabilities: drop: - "ALL" livenessProbe: httpGet: path: /healthz port: 8081 initialDelaySeconds: 15 periodSeconds: 20 readinessProbe: httpGet: path: /readyz port: 8081 initialDelaySeconds: 5 periodSeconds: 10 # TODO(user): Configure the resources accordingly based on the project requirements. # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: cpu: 500m memory: 128Mi requests: cpu: 10m memory: 64Mi volumeMounts: [] volumes: [] serviceAccountName: controller-manager terminationGracePeriodSeconds: 10 ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/manager/kustomization.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package manager import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the manager folder type Kustomization struct { machinery.TemplateMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "manager", "kustomization.yaml") } f.TemplateBody = kustomizeManagerTemplate f.IfExistsAction = machinery.Error return nil } const kustomizeManagerTemplate = `resources: - manager.yaml ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/network-policy/allow-metrics-traffic.go ================================================ /* Copyright 2024 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 networkpolicy import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &PolicyAllowMetrics{} // PolicyAllowMetrics scaffolds a file that defines the NetworkPolicy // to allow access to the metrics endpoint type PolicyAllowMetrics struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *PolicyAllowMetrics) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "network-policy", "allow-metrics-traffic.yaml") } f.TemplateBody = metricsNetworkPolicyTemplate return nil } const metricsNetworkPolicyTemplate = `# This NetworkPolicy allows ingress traffic # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those # namespaces are able to gather data from the metrics endpoint. apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: allow-metrics-traffic namespace: system spec: podSelector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} policyTypes: - Ingress ingress: # This allows ingress traffic from any namespace with the label metrics: enabled - from: - namespaceSelector: matchLabels: metrics: enabled # Only from namespaces with this label ports: - port: 8443 protocol: TCP ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/network-policy/allow-webhook-traffic.go ================================================ /* Copyright 2024 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 networkpolicy import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &PolicyAllowWebhooks{} // PolicyAllowWebhooks in scaffolds a file that defines the NetworkPolicy // to allow the webhook server can communicate type PolicyAllowWebhooks struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *PolicyAllowWebhooks) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "network-policy", "allow-webhook-traffic.yaml") } f.TemplateBody = webhooksNetworkPolicyTemplate return nil } const webhooksNetworkPolicyTemplate = `# This NetworkPolicy allows ingress traffic to your webhook server running # as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks # will only work when applied in namespaces labeled with 'webhook: enabled' apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: allow-webhook-traffic namespace: system spec: podSelector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} policyTypes: - Ingress ingress: # This allows ingress traffic from any namespace with the label webhook: enabled - from: - namespaceSelector: matchLabels: webhook: enabled # Only from namespaces with this label ports: - port: 443 protocol: TCP ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/network-policy/kustomization.go ================================================ /* Copyright 2024 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 networkpolicy import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder type Kustomization struct { machinery.TemplateMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "network-policy", "kustomization.yaml") } f.TemplateBody = kustomizationTemplate return nil } const kustomizationTemplate = `resources: - allow-metrics-traffic.yaml ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/prometheus/kustomization.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package prometheus import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the prometheus folder type Kustomization struct { machinery.TemplateMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "prometheus", "kustomization.yaml") } f.TemplateBody = kustomizationTemplate return nil } const kustomizationTemplate = `resources: - monitor.yaml # [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus # to securely reference certificates created and managed by cert-manager. # Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml # to mount the "metrics-server-cert" secret in the Manager Deployment. #patches: # - path: monitor_tls_patch.yaml # target: # kind: ServiceMonitor ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/prometheus/monitor.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package prometheus import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Monitor{} // Monitor scaffolds a file that defines the prometheus service monitor type Monitor struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *Monitor) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "prometheus", "monitor.yaml") } f.TemplateBody = serviceMonitorTemplate return nil } //nolint:lll const serviceMonitorTemplate = `# Prometheus Monitor Service (Metrics) apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: labels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: controller-manager-metrics-monitor namespace: system spec: endpoints: - path: /metrics port: https # Ensure this is the name of the port that exposes HTTPS metrics scheme: https bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token tlsConfig: # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables # certificate verification, exposing the system to potential man-in-the-middle attacks. # For production environments, it is recommended to use cert-manager for automatic TLS certificate management. # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml, # which securely references the certificate from the 'metrics-server-cert' secret. insecureSkipVerify: true selector: matchLabels: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/prometheus/monitor_tls_patch.go ================================================ /* Copyright 2024 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 prometheus import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &ServiceMonitorPatch{} // ServiceMonitorPatch scaffolds a file that defines the patch for the ServiceMonitor // to use cert-manager managed certificates for secure TLS configuration. type ServiceMonitorPatch struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *ServiceMonitorPatch) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "prometheus", "monitor_tls_patch.yaml") } f.TemplateBody = serviceMonitorPatchTemplate return nil } const serviceMonitorPatchTemplate = `# Patch for Prometheus ServiceMonitor to enable secure TLS configuration # using certificates managed by cert-manager - op: replace path: /spec/endpoints/0/tlsConfig value: # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc insecureSkipVerify: false ca: secret: name: metrics-server-cert key: ca.crt cert: secret: name: metrics-server-cert key: tls.crt keySecret: name: metrics-server-cert key: tls.key ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/cluster_role.go ================================================ /* Copyright 2026 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 rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &ClusterRole{} // ClusterRole scaffolds a ClusterRole for the manager type ClusterRole struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *ClusterRole) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "role.yaml") } f.TemplateBody = clusterRoleTemplate f.IfExistsAction = machinery.OverwriteFile return nil } const clusterRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: manager-role rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"] ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/cluster_role_binding.go ================================================ /* Copyright 2026 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 rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &ClusterRoleBinding{} // ClusterRoleBinding scaffolds a ClusterRoleBinding for the manager type ClusterRoleBinding struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *ClusterRoleBinding) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "role_binding.yaml") } f.TemplateBody = clusterRoleBindingTemplate f.IfExistsAction = machinery.OverwriteFile return nil } const clusterRoleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: manager-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/crd_admin_role.go ================================================ /* Copyright 2024 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. */ //nolint:dupl package rbac import ( "fmt" "path/filepath" "strings" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &CRDAdminRole{} // CRDAdminRole scaffolds a file that defines the role that allows full control over plurals type CRDAdminRole struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.ResourceMixin machinery.ProjectNameMixin machinery.NamespacedMixin RoleName string } // SetTemplateDefaults implements machinery.Template func (f *CRDAdminRole) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("config", "rbac", "%[group]_%[kind]_admin_role.yaml") } else { f.Path = filepath.Join("config", "rbac", "%[kind]_admin_role.yaml") } } f.Path = f.Resource.Replacer().Replace(f.Path) if f.RoleName == "" { if f.MultiGroup && f.Resource.Group != "" { f.RoleName = fmt.Sprintf("%s-%s-admin-role", strings.ToLower(f.Resource.Group), strings.ToLower(f.Resource.Kind)) } else { f.RoleName = fmt.Sprintf("%s-admin-role", strings.ToLower(f.Resource.Kind)) } } f.TemplateBody = crdRoleAdminTemplate f.IfExistsAction = machinery.OverwriteFile return nil } const crdRoleAdminTemplate = `# This rule is not used by the project {{ .ProjectName }} itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants full permissions ('*') over {{ .Resource.QualifiedGroup }}. # This role is intended for users authorized to modify roles and bindings within the cluster, # enabling them to delegate specific permissions to other users or groups as needed. apiVersion: rbac.authorization.k8s.io/v1 kind: {{ if .Namespaced }}Role{{ else }}ClusterRole{{ end }} metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: {{ .RoleName }} rules: - apiGroups: - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }} verbs: - '*' - apiGroups: - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }}/status verbs: - get ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/crd_editor_role.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ //nolint:dupl package rbac import ( "fmt" "path/filepath" "strings" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &CRDEditorRole{} // CRDEditorRole scaffolds a file that defines the role that allows to edit plurals type CRDEditorRole struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.ResourceMixin machinery.ProjectNameMixin machinery.NamespacedMixin RoleName string } // SetTemplateDefaults implements machinery.Template func (f *CRDEditorRole) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("config", "rbac", "%[group]_%[kind]_editor_role.yaml") } else { f.Path = filepath.Join("config", "rbac", "%[kind]_editor_role.yaml") } } f.Path = f.Resource.Replacer().Replace(f.Path) if f.RoleName == "" { if f.MultiGroup && f.Resource.Group != "" { f.RoleName = fmt.Sprintf("%s-%s-editor-role", strings.ToLower(f.Resource.Group), strings.ToLower(f.Resource.Kind)) } else { f.RoleName = fmt.Sprintf("%s-editor-role", strings.ToLower(f.Resource.Kind)) } } f.TemplateBody = crdRoleEditorTemplate f.IfExistsAction = machinery.OverwriteFile return nil } const crdRoleEditorTemplate = `# This rule is not used by the project {{ .ProjectName }} itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants permissions to create, update, and delete resources within the {{ .Resource.QualifiedGroup }}. # This role is intended for users who need to manage these resources # but should not control RBAC or manage permissions for others. apiVersion: rbac.authorization.k8s.io/v1 kind: {{ if .Namespaced }}Role{{ else }}ClusterRole{{ end }} metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: {{ .RoleName }} rules: - apiGroups: - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }} verbs: - create - delete - get - list - patch - update - watch - apiGroups: - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }}/status verbs: - get ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/crd_viewer_role.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ //nolint:dupl package rbac import ( "fmt" "path/filepath" "strings" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &CRDViewerRole{} // CRDViewerRole scaffolds a file that defines the role that allows to view plurals type CRDViewerRole struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.ResourceMixin machinery.ProjectNameMixin machinery.NamespacedMixin RoleName string } // SetTemplateDefaults implements machinery.Template func (f *CRDViewerRole) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("config", "rbac", "%[group]_%[kind]_viewer_role.yaml") } else { f.Path = filepath.Join("config", "rbac", "%[kind]_viewer_role.yaml") } } f.Path = f.Resource.Replacer().Replace(f.Path) if f.RoleName == "" { if f.MultiGroup && f.Resource.Group != "" { f.RoleName = fmt.Sprintf("%s-%s-viewer-role", strings.ToLower(f.Resource.Group), strings.ToLower(f.Resource.Kind)) } else { f.RoleName = fmt.Sprintf("%s-viewer-role", strings.ToLower(f.Resource.Kind)) } } f.TemplateBody = crdRoleViewerTemplate f.IfExistsAction = machinery.OverwriteFile return nil } const crdRoleViewerTemplate = `# This rule is not used by the project {{ .ProjectName }} itself. # It is provided to allow the cluster admin to help manage permissions for users. # # Grants read-only access to {{ .Resource.QualifiedGroup }} resources. # This role is intended for users who need visibility into these resources # without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. apiVersion: rbac.authorization.k8s.io/v1 kind: {{ if .Namespaced }}Role{{ else }}ClusterRole{{ end }} metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: {{ .RoleName }} rules: - apiGroups: - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }} verbs: - get - list - watch - apiGroups: - {{ .Resource.QualifiedGroup }} resources: - {{ .Resource.Plural }}/status verbs: - get ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/kustomization.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the rbac folder type Kustomization struct { machinery.TemplateMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "kustomization.yaml") } f.TemplateBody = kustomizeRBACTemplate f.IfExistsAction = machinery.Error return nil } const kustomizeRBACTemplate = `resources: # All RBAC will be applied under this service account in # the deployment namespace. You may comment out this resource # if your manager will use a service account that exists at # runtime. Be sure to update RoleBinding and ClusterRoleBinding # subjects if changing service account names. - service_account.yaml - role.yaml - role_binding.yaml - leader_election_role.yaml - leader_election_role_binding.yaml # The following RBAC configurations are used to protect # the metrics endpoint with authn/authz. These configurations # ensure that only authorized users and service accounts # can access the metrics endpoint. Comment the following # permissions if you want to disable this protection. # More info: https://book.kubebuilder.io/reference/metrics.html - metrics_auth_role.yaml - metrics_auth_role_binding.yaml - metrics_reader_role.yaml ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/leader_election_role.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &LeaderElectionRole{} // LeaderElectionRole scaffolds a file that defines the role that allows leader election type LeaderElectionRole struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *LeaderElectionRole) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "leader_election_role.yaml") } f.TemplateBody = leaderElectionRoleTemplate return nil } const leaderElectionRoleTemplate = `# permissions to do leader election. apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: leader-election-role rules: - apiGroups: - "" resources: - configmaps verbs: - get - list - watch - create - update - patch - delete - apiGroups: - coordination.k8s.io resources: - leases verbs: - get - list - watch - create - update - patch - delete - apiGroups: - "" resources: - events verbs: - create - patch ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/leader_election_role_binding.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &LeaderElectionRoleBinding{} // LeaderElectionRoleBinding scaffolds a file that defines the role binding that allows leader election type LeaderElectionRoleBinding struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *LeaderElectionRoleBinding) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "leader_election_role_binding.yaml") } f.TemplateBody = leaderElectionRoleBindingTemplate return nil } const leaderElectionRoleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: leader-election-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: leader-election-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/metrics_auth_role.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &MetricsAuthRole{} // MetricsAuthRole scaffolds a file that defines the role for the auth proxy type MetricsAuthRole struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *MetricsAuthRole) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "metrics_auth_role.yaml") } f.TemplateBody = metricsAuthRoleTemplate return nil } const metricsAuthRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-auth-role rules: - apiGroups: - authentication.k8s.io resources: - tokenreviews verbs: - create - apiGroups: - authorization.k8s.io resources: - subjectaccessreviews verbs: - create ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/metrics_auth_role_binding.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &MetricsAuthRole{} // MetricsAuthRoleBinding scaffolds a file that defines the role binding for the auth proxy type MetricsAuthRoleBinding struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *MetricsAuthRoleBinding) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "metrics_auth_role_binding.yaml") } f.TemplateBody = metricsAuthRoleBindingTemplate return nil } const metricsAuthRoleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: metrics-auth-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: metrics-auth-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/metrics_reader_role.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &MetricsReaderRole{} // MetricsReaderRole scaffolds a file that defines the role for the auth proxy type MetricsReaderRole struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *MetricsReaderRole) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "metrics_reader_role.yaml") } f.TemplateBody = metricsReaderRoleTemplate return nil } const metricsReaderRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: metrics-reader rules: - nonResourceURLs: - "/metrics" verbs: - get ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/namespaced_role.go ================================================ /* Copyright 2026 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 rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &NamespacedRole{} // NamespacedRole scaffolds a namespace-scoped Role for the manager type NamespacedRole struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *NamespacedRole) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "role.yaml") } f.TemplateBody = namespacedRoleTemplate f.IfExistsAction = machinery.OverwriteFile return nil } const namespacedRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: manager-role rules: - apiGroups: [""] resources: ["pods"] verbs: ["get", "list", "watch"] ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/namespaced_role_binding.go ================================================ /* Copyright 2026 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 rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &NamespacedRoleBinding{} // NamespacedRoleBinding scaffolds a namespace-scoped RoleBinding for the manager type NamespacedRoleBinding struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *NamespacedRoleBinding) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "role_binding.yaml") } f.TemplateBody = namespacedRoleBindingTemplate f.IfExistsAction = machinery.OverwriteFile return nil } const namespacedRoleBindingTemplate = `apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: manager-role subjects: - kind: ServiceAccount name: controller-manager namespace: system ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/rbac/service_account.go ================================================ /* Copyright 2022 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 rbac import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &ServiceAccount{} // ServiceAccount scaffolds a file that defines the service account the manager is deployed in. type ServiceAccount struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *ServiceAccount) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "rbac", "service_account.yaml") } f.TemplateBody = serviceAccountTemplate return nil } const serviceAccountTemplate = `apiVersion: v1 kind: ServiceAccount metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: controller-manager namespace: system ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/samples/crd_sample.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package samples import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &CRDSample{} // CRDSample scaffolds a file that defines a sample manifest for the CRD type CRDSample struct { machinery.TemplateMixin machinery.ResourceMixin machinery.ProjectNameMixin Force bool } // SetTemplateDefaults implements machinery.Template func (f *CRDSample) SetTemplateDefaults() error { if f.Path == "" { if f.Resource.Group != "" { f.Path = filepath.Join("config", "samples", "%[group]_%[version]_%[kind].yaml") } else { f.Path = filepath.Join("config", "samples", "%[version]_%[kind].yaml") } } f.Path = f.Resource.Replacer().Replace(f.Path) if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { f.IfExistsAction = machinery.Error } f.TemplateBody = crdSampleTemplate return nil } const crdSampleTemplate = `apiVersion: {{ .Resource.QualifiedGroup }}/{{ .Resource.Version }} kind: {{ .Resource.Kind }} metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: {{ lower .Resource.Kind }}-sample spec: # TODO(user): Add fields here ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/samples/kustomization.go ================================================ /* Copyright 2022 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 samples import ( "fmt" "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var ( _ machinery.Template = &Kustomization{} _ machinery.Inserter = &Kustomization{} ) // Kustomization scaffolds a kustomization.yaml for the manifests overlay folder. type Kustomization struct { machinery.TemplateMixin machinery.ResourceMixin } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "samples", "kustomization.yaml") } f.TemplateBody = fmt.Sprintf(kustomizationTemplate, machinery.NewMarkerFor(f.Path, samplesMarker)) return nil } const ( samplesMarker = "manifestskustomizesamples" ) // GetMarkers implements file.Inserter func (f *Kustomization) GetMarkers() []machinery.Marker { return []machinery.Marker{machinery.NewMarkerFor(f.Path, samplesMarker)} } const samplesCodeFragment = `- %s ` // makeCRFileName returns a Custom Resource example file name in the same format // as kubebuilder's CreateAPI plugin for a gvk. func (f Kustomization) makeCRFileName() string { if f.Resource.Group != "" { return f.Resource.Replacer().Replace("%[group]_%[version]_%[kind].yaml") } return f.Resource.Replacer().Replace("%[version]_%[kind].yaml") } // GetCodeFragments implements file.Inserter func (f *Kustomization) GetCodeFragments() machinery.CodeFragmentsMap { return machinery.CodeFragmentsMap{ machinery.NewMarkerFor(f.Path, samplesMarker): []string{fmt.Sprintf(samplesCodeFragment, f.makeCRFileName())}, } } const kustomizationTemplate = `## Append samples of your project ## resources: %s ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/webhook/kustomization.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package webhook import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Kustomization{} // Kustomization scaffolds a file that defines the kustomization scheme for the webhook folder type Kustomization struct { machinery.TemplateMixin machinery.ResourceMixin Force bool } // SetTemplateDefaults implements machinery.Template func (f *Kustomization) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "webhook", "kustomization.yaml") } f.TemplateBody = kustomizeWebhookTemplate if f.Force { f.IfExistsAction = machinery.OverwriteFile } else { // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = machinery.SkipFile } return nil } const kustomizeWebhookTemplate = `resources: - manifests{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}.{{ .Resource.Webhooks.WebhookVersion }}{{ end }}.yaml - service.yaml ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/webhook/service.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package webhook import ( "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Service{} // Service scaffolds a file that defines the webhook service type Service struct { machinery.TemplateMixin machinery.ProjectNameMixin } // SetTemplateDefaults implements machinery.Template func (f *Service) SetTemplateDefaults() error { if f.Path == "" { f.Path = filepath.Join("config", "webhook", "service.yaml") } f.TemplateBody = serviceTemplate // If file exists (ex. because a webhook was already created), skip creation. f.IfExistsAction = machinery.SkipFile return nil } const serviceTemplate = `apiVersion: v1 kind: Service metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: webhook-service namespace: system spec: ports: - port: 443 protocol: TCP targetPort: 9443 selector: control-plane: controller-manager app.kubernetes.io/name: {{ .ProjectName }} ` ================================================ FILE: pkg/plugins/common/kustomize/v2/scaffolds/webhook.go ================================================ /* Copyright 2022 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 scaffolds import ( "errors" "fmt" log "log/slog" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/pkg/plugins" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/certmanager" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/crd/patches" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/kdefault" networkpolicy "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/network-policy" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds/internal/templates/config/webhook" ) var _ plugins.Scaffolder = &webhookScaffolder{} const ( kustomizeFilePath = "config/default/kustomization.yaml" kustomizeCRDFilePath = "config/crd/kustomization.yaml" ) type webhookScaffolder struct { config config.Config resource resource.Resource // fs is the filesystem that will be used by the scaffolder fs machinery.Filesystem // force indicates whether to scaffold files even if they exist. force bool } // NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations func NewWebhookScaffolder(cfg config.Config, res resource.Resource, force bool) plugins.Scaffolder { return &webhookScaffolder{ config: cfg, resource: res, force: force, } } // InjectFS implements cmdutil.Scaffolder func (s *webhookScaffolder) InjectFS(fs machinery.Filesystem) { s.fs = fs } // Scaffold implements cmdutil.Scaffolder func (s *webhookScaffolder) Scaffold() error { log.Info("Writing kustomize manifests for you to edit...") // Will validate the scaffold // Users that scaffolded the project previously // with the bugs will receive a message to help // them out fix their scaffold. validateScaffoldedProject() // Initialize the machinery.Scaffold that will write the files to disk scaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config), machinery.WithResource(&s.resource), ) if err := s.config.UpdateResource(s.resource); err != nil { return fmt.Errorf("error updating resource: %w", err) } buildScaffold := []machinery.Builder{ &kdefault.ManagerWebhookPatch{}, &webhook.Kustomization{Force: s.force}, &webhook.Service{}, &certmanager.Certificate{}, &certmanager.Issuer{}, &certmanager.MetricsCertificate{}, &certmanager.Kustomization{}, &certmanager.KustomizeConfig{}, &networkpolicy.PolicyAllowWebhooks{}, } // Only scaffold the following patches if is a conversion webhook if s.resource.Webhooks.Conversion { buildScaffold = append(buildScaffold, &patches.EnableWebhookPatch{}) buildScaffold = append(buildScaffold, &kdefault.KustomizationCAConversionUpdater{}) } if !s.resource.External && !s.resource.Core { buildScaffold = append(buildScaffold, &crd.Kustomization{}) } if err := scaffold.Execute(buildScaffold...); err != nil { return fmt.Errorf("error scaffolding kustomize webhook manifests: %w", err) } // Warn users about potential bootstrap problem for core type webhooks if s.resource.Core { log.Warn("Webhooks for core types may cause circular dependencies during deployment. " + "More info: https://book.kubebuilder.io/reference/webhook-bootstrap-problem") } // Apply project-specific customizations: // - Add reference to allow-webhook-traffic.yaml in network policy configuration. // - Enable all webhook-related sections in config/default/kustomization.yaml. addNetworkPoliciesForWebhooks() // enableWebhookDefaults ensures all necessary components for webhook functionality // are enabled in config/default/kustomization.yaml, including: // - webhook and cert-manager directories // - manager patches // - replacements for certificate injection enableWebhookDefaults() if s.resource.HasValidationWebhook() { uncommentCodeForValidationWebhooks() } if s.resource.HasDefaultingWebhook() { uncommentCodeForDefaultWebhooks() } if s.resource.HasConversionWebhook() { uncommentCodeForConversionWebhooks(s.resource) } const helmPluginKey = "helm.kubebuilder.io/v1-alpha" var helmPlugin any err := s.config.DecodePluginConfig(helmPluginKey, &helmPlugin) if !errors.As(err, &config.PluginKeyNotFoundError{}) { testChartPath := ".github/workflows/test-chart.yml" //nolint:lll _ = pluginutil.UncommentCode( testChartPath, `# - name: Install cert-manager via Helm # run: | # helm repo add jetstack https://charts.jetstack.io # helm repo update # helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true # # - name: Wait for cert-manager to be ready # run: | # kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager # kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector # kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook `, "#", ) _ = pluginutil.ReplaceInFile(testChartPath, "# TODO: Uncomment if cert-manager is enabled", "") } return nil } // uncommentCodeForConversionWebhooks enables CA injection logic in Kustomize manifests // for ConversionWebhooks by uncommenting certificate sources and CRD annotation targets. // This is required to make cert-manager correctly inject the CA bundle into CRDs. func uncommentCodeForConversionWebhooks(r resource.Resource) { crdName := fmt.Sprintf("%s.%s", r.Plural, r.QualifiedGroup()) err := pluginutil.UncommentCode( kustomizeFilePath, fmt.Sprintf(`# - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # - select: # kind: CustomResourceDefinition # name: %s # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true`, crdName), "#", ) if err != nil { log.Warn("Unable to find the certificate namespace replacement for "+ "CRD to uncomment in the file. Conversion webhooks require this replacement "+ "to inject the CA properly.", "crdName", crdName, "file", kustomizeFilePath) } err = pluginutil.UncommentCode( kustomizeFilePath, fmt.Sprintf(`# - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # - select: # kind: CustomResourceDefinition # name: %s # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true`, crdName), "#", ) if err != nil { log.Warn("Unable to find the certificate name replacement for CRD "+ "to uncomment in the file. Conversion webhooks require this replacement to inject "+ "the CA properly.", "crdName", crdName, "file", kustomizeFilePath) } err = pluginutil.UncommentCode(kustomizeCRDFilePath, `#configurations: #- kustomizeconfig.yaml`, `#`) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeCRDFilePath, `configurations: - kustomizeconfig.yaml`) if !hasWebHookUncommented || errCheck != nil { log.Warn("unable to find the target configurations with kustomizeconfig.yaml "+ "to uncomment in the file; conversion webhooks require this configuration "+ "to be uncommented to inject CA", "file", kustomizeCRDFilePath) } } } func uncommentCodeForDefaultWebhooks() { err := pluginutil.UncommentCode( kustomizeFilePath, `# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting ) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # - select: # kind: MutatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # - select: # kind: MutatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true`, "#", ) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, ` targets: - select: kind: MutatingWebhookConfiguration`) if !hasWebHookUncommented || errCheck != nil { log.Warn("unable to find the MutatingWebhookConfiguration section "+ "to uncomment in the file. Webhooks scaffolded with '--defaulting' require "+ "this configuration for CA injection", "file", kustomizeFilePath) } } } func uncommentCodeForValidationWebhooks() { err := pluginutil.UncommentCode( kustomizeFilePath, `# - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # This name should match the one in certificate.yaml # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # - select: # kind: ValidatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 0 # create: true # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPath: .metadata.name # targets: # - select: # kind: ValidatingWebhookConfiguration # fieldPaths: # - .metadata.annotations.[cert-manager.io/inject-ca-from] # options: # delimiter: '/' # index: 1 # create: true`, "#", ) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, ` targets: - select: kind: ValidatingWebhookConfiguration`) if !hasWebHookUncommented || errCheck != nil { log.Warn("unable to find the ValidatingWebhookConfiguration section "+ "to uncomment in the file. Webhooks scaffolded with '--programmatic-validation' "+ "require this configuration for CA injection", "file", kustomizeFilePath) } } } func enableWebhookDefaults() { err := pluginutil.UncommentCode(kustomizeFilePath, "#- ../webhook", `#`) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, "- ../webhook") if !hasWebHookUncommented || errCheck != nil { log.Warn("unable to find the target #- ../webhook to uncomment in the file", "file", kustomizeFilePath) } } err = pluginutil.UncommentCode(kustomizeFilePath, "#patches:", `#`) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, "patches:") if !hasWebHookUncommented || errCheck != nil { log.Warn("unable to find the line '#patches:' to uncomment in the file", "file", kustomizeFilePath) } } err = pluginutil.UncommentCode(kustomizeFilePath, `#- path: manager_webhook_patch.yaml # target: # kind: Deployment`, `#`) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, "- path: manager_webhook_patch.yaml") if !hasWebHookUncommented || errCheck != nil { log.Warn("unable to find the target #- path: manager_webhook_patch.yaml to uncomment in the file", "file", kustomizeFilePath) } } err = pluginutil.UncommentCode(kustomizeFilePath, `#- ../certmanager`, `#`) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, "../certmanager") if !hasWebHookUncommented || errCheck != nil { log.Warn("unable to find the '../certmanager' section to uncomment in the file. "+ "Projects that use webhooks must enable certificate management; "+ "Please ensure cert-manager integration is enabled", "file", kustomizeFilePath) } } err = pluginutil.UncommentCode(kustomizeFilePath, `#replacements:`, `#`) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, "replacements:") if !hasWebHookUncommented || errCheck != nil { log.Warn("Unable to find the '#replacements:' section to uncomment in the file"+ "Projects using webhooks must enable cert-manager CA injection by uncommenting"+ "the required replacements.", "file", kustomizeFilePath) } } err = pluginutil.UncommentCode( kustomizeFilePath, `# - source: # Uncomment the following block if you have any webhook # kind: Service # version: v1 # name: webhook-service # fieldPath: .metadata.name # Name of the service # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPaths: # - .spec.dnsNames.0 # - .spec.dnsNames.1 # options: # delimiter: '.' # index: 0 # create: true # - source: # kind: Service # version: v1 # name: webhook-service # fieldPath: .metadata.namespace # Namespace of the service # targets: # - select: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # fieldPaths: # - .spec.dnsNames.0 # - .spec.dnsNames.1 # options: # delimiter: '.' # index: 1 # create: true`, "#", ) if err != nil { hasWebHookUncommented, errCheck := pluginutil.HasFileContentWith(kustomizeFilePath, ` kind: Service version: v1 name: webhook-service fieldPath: .metadata.name`) if !hasWebHookUncommented || errCheck != nil { log.Warn("Unable to find the '#- source: # Uncomment the following block if you have any webhook' "+ "section to uncomment in the file. "+ "Projects with webhooks must enable certificates via cert-manager.", "file", kustomizeFilePath) } } } func addNetworkPoliciesForWebhooks() { policyKustomizeFilePath := "config/network-policy/kustomization.yaml" err := pluginutil.InsertCodeIfNotExist(policyKustomizeFilePath, "resources:", allowWebhookTrafficFragment) if err != nil { log.Error("failed to add the line '- allow-webhook-traffic.yaml' at the end of the file "+ "to allow webhook traffic", "file", policyKustomizeFilePath) } } // Deprecated: remove it when go/v4 and/or kustomize/v2 be removed // validateScaffoldedProject will output a message to help users fix their scaffold func validateScaffoldedProject() { hasCertManagerPatch, _ := pluginutil.HasFileContentWith(kustomizeFilePath, "crdkustomizecainjectionpatch") if hasCertManagerPatch { log.Warn(` 1. **Remove the CERTMANAGER Section from config/crd/kustomization.yaml:** Delete the CERTMANAGER section to prevent unintended CA injection patches for CRDs. Ensure the following lines are removed or commented out: # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. # patches here are for enabling the CA injection for each CRD #- path: patches/cainjection_in_firstmates.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch 2. **Ensure CA Injection Configuration in config/default/kustomization.yaml:** Under the [CERTMANAGER] replacement in config/default/kustomization.yaml, add the following code for proper CA injection generation: **NOTE:** You must ensure that the code contains the following target markers: - +kubebuilder:scaffold:crdkustomizecainjectionns - +kubebuilder:scaffold:crdkustomizecainjectioname # - source: # Uncomment the following block if you have a ConversionWebhook (--conversion) # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # This name should match the one in certificate.yaml # fieldPath: .metadata.namespace # Namespace of the certificate CR # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectionns # - source: # kind: Certificate # group: cert-manager.io # version: v1 # name: serving-cert # This name should match the one in certificate.yaml # fieldPath: .metadata.name # targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD. # +kubebuilder:scaffold:crdkustomizecainjectioname 3. **Ensure Only Conversion Webhook Patches in config/crd/patches:** The config/crd/patches directory and the corresponding entries in config/crd/kustomization.yaml should only contain files for conversion webhooks. Previously, a bug caused the patch file to be generated for any webhook, but only patches for webhooks created with the --conversion option should be included. For further guidance, you can refer to examples in the testdata/ directory in the Kubebuilder repository. **Alternatively**: You can use the 'alpha generate' command to re-generate the project from scratch using the latest release available. Afterward, you can re-add only your code implementation on top to ensure your project includes all the latest bug fixes and enhancements. `) } } const allowWebhookTrafficFragment = ` - allow-webhook-traffic.yaml` ================================================ FILE: pkg/plugins/common/kustomize/v2/suite_test.go ================================================ /* Copyright 2026 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 v2 import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestKustomizeV2Plugin(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Kustomize V2 Plugin Suite") } ================================================ FILE: pkg/plugins/common/kustomize/v2/webhook.go ================================================ /* Copyright 2022 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 v2 import ( "fmt" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds" ) var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} type createWebhookSubcommand struct { createSubcommand } func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error { scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force) scaffolder.InjectFS(fs) if err := scaffolder.Scaffold(); err != nil { return fmt.Errorf("failed to scaffold webhook subcommand: %w", err) } return nil } ================================================ FILE: pkg/plugins/domain.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package plugins // DefaultNameQualifier is the suffix appended to all kubebuilder plugin names. const DefaultNameQualifier = "kubebuilder.io" ================================================ FILE: pkg/plugins/external/api.go ================================================ /* Copyright 2021 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 external import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" ) var _ plugin.CreateAPISubcommand = &createAPISubcommand{} const ( defaultAPIVersion = "v1alpha1" ) type createAPISubcommand struct { Path string Args []string pluginChain []string config config.Config } // InjectConfig injects the project configuration so external plugins can read the PROJECT file. func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c if c == nil { return nil } if chain := c.GetPluginChain(); len(chain) > 0 { p.pluginChain = append([]string(nil), chain...) } return nil } func (p *createAPISubcommand) SetPluginChain(chain []string) { if len(chain) == 0 { p.pluginChain = nil return } p.pluginChain = append([]string(nil), chain...) } func (p *createAPISubcommand) InjectResource(*resource.Resource) error { // Do nothing since resource flags are passed to the external plugin directly. return nil } func (p *createAPISubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { setExternalPluginMetadata("api", p.Path, subcmdMeta) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "api", p.Path, p.Args) } func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error { req := external.PluginRequest{ APIVersion: defaultAPIVersion, Command: "create api", Args: p.Args, PluginChain: p.pluginChain, } err := handlePluginResponse(fs, req, p.Path, p.config) if err != nil { return err } return nil } ================================================ FILE: pkg/plugins/external/edit.go ================================================ /* Copyright 2021 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. */ //nolint:dupl package external import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" ) var _ plugin.EditSubcommand = &editSubcommand{} type editSubcommand struct { Path string Args []string pluginChain []string config config.Config } // InjectConfig injects the project configuration to access plugin chain information func (p *editSubcommand) InjectConfig(c config.Config) error { p.config = c if c == nil { return nil } if chain := c.GetPluginChain(); len(chain) > 0 { p.pluginChain = append([]string(nil), chain...) } return nil } func (p *editSubcommand) SetPluginChain(chain []string) { if len(chain) == 0 { p.pluginChain = nil return } p.pluginChain = append([]string(nil), chain...) } func (p *editSubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { setExternalPluginMetadata("edit", p.Path, subcmdMeta) } func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "edit", p.Path, p.Args) } func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error { req := external.PluginRequest{ APIVersion: defaultAPIVersion, Command: "edit", Args: p.Args, PluginChain: p.pluginChain, } err := handlePluginResponse(fs, req, p.Path, p.config) if err != nil { return err } return nil } ================================================ FILE: pkg/plugins/external/external_test.go ================================================ /* Copyright 2022 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 external import ( "encoding/json" "fmt" "os" "path/filepath" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" v3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" ) type chainAwareSubcommand interface { SetPluginChain([]string) InjectConfig(config.Config) error } var pluginChainTestCases = []struct { name string new func() chainAwareSubcommand get func(chainAwareSubcommand) []string }{ { name: "init", new: func() chainAwareSubcommand { return &initSubcommand{} }, get: func(sub chainAwareSubcommand) []string { return sub.(*initSubcommand).pluginChain }, }, { name: "edit", new: func() chainAwareSubcommand { return &editSubcommand{} }, get: func(sub chainAwareSubcommand) []string { return sub.(*editSubcommand).pluginChain }, }, { name: "create api", new: func() chainAwareSubcommand { return &createAPISubcommand{} }, get: func(sub chainAwareSubcommand) []string { return sub.(*createAPISubcommand).pluginChain }, }, { name: "create webhook", new: func() chainAwareSubcommand { return &createWebhookSubcommand{} }, get: func(sub chainAwareSubcommand) []string { return sub.(*createWebhookSubcommand).pluginChain }, }, } func TestExternalPlugin(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Scaffold") } type mockValidOutputGetter struct{} type mockInValidOutputGetter struct{} var _ ExecOutputGetter = &mockValidOutputGetter{} func (m *mockValidOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { return []byte(`{ "command": "init", "error": false, "error_msg": "none", "universe": {"LICENSE": "Apache 2.0 License\n"} }`), nil } var _ ExecOutputGetter = &mockInValidOutputGetter{} func (m *mockInValidOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { return nil, fmt.Errorf("error getting exec command output") } type mockPluginChainCaptureGetter struct { capturedChain *[]string } var _ ExecOutputGetter = &mockPluginChainCaptureGetter{} func (m *mockPluginChainCaptureGetter) GetExecOutput(request []byte, _ string) ([]byte, error) { // Parse the request to capture the plugin chain var req external.PluginRequest if err := json.Unmarshal(request, &req); err != nil { return nil, fmt.Errorf("error unmarshalling request: %w", err) } // Capture the plugin chain *m.capturedChain = req.PluginChain // Return a valid response return []byte(`{ "command": "init", "error": false, "error_msg": "none", "universe": {"LICENSE": "Apache 2.0 License\n"} }`), nil } type mockValidOsWdGetter struct{} var _ OsWdGetter = &mockValidOsWdGetter{} func (m *mockValidOsWdGetter) GetCurrentDir() (string, error) { return "tmp/externalPlugin", nil } type mockInValidOsWdGetter struct{} var _ OsWdGetter = &mockInValidOsWdGetter{} func (m *mockInValidOsWdGetter) GetCurrentDir() (string, error) { return "", fmt.Errorf("error getting current directory") } type mockConfigOutputGetter struct { capturedRequest *external.PluginRequest } var _ ExecOutputGetter = &mockConfigOutputGetter{} func (m *mockConfigOutputGetter) GetExecOutput(reqBytes []byte, _ string) ([]byte, error) { m.capturedRequest = &external.PluginRequest{} if err := json.Unmarshal(reqBytes, m.capturedRequest); err != nil { return nil, fmt.Errorf("error unmarshalling request: %w", err) } return []byte(`{ "command": "init", "error": false, "error_msg": "none", "universe": {"LICENSE": "Apache 2.0 License\n"} }`), nil } type mockValidFlagOutputGetter struct{} func (m *mockValidFlagOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { response := external.PluginResponse{ Command: "flag", Error: false, Universe: nil, Flags: getFlags(), } marshaledResponse, err := json.Marshal(response) if err != nil { return nil, fmt.Errorf("error marshalling response: %w", err) } return marshaledResponse, nil } type mockValidMEOutputGetter struct{} func (m *mockValidMEOutputGetter) GetExecOutput(_ []byte, _ string) ([]byte, error) { response := external.PluginResponse{ Command: "metadata", Error: false, Universe: nil, Metadata: getMetadata(), } marshaledResponse, err := json.Marshal(response) if err != nil { return nil, fmt.Errorf("error marshalling response: %w", err) } return marshaledResponse, nil } const ( externalPlugin = "myexternalplugin.sh" floatVal = "float" ) var _ = Describe("Run external plugin using Scaffold", func() { Context("with valid mock values", func() { const filePerm os.FileMode = 755 var ( pluginFileName string args []string f afero.File fs machinery.Filesystem err error ) BeforeEach(func() { outputGetter = &mockValidOutputGetter{} currentDirGetter = &mockValidOsWdGetter{} fs = machinery.Filesystem{ FS: afero.NewMemMapFs(), } pluginFileName = "externalPlugin.sh" pluginFilePath := filepath.Join("tmp", "externalPlugin", pluginFileName) err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), filePerm) Expect(err).ToNot(HaveOccurred()) f, err = fs.FS.Create(pluginFilePath) Expect(err).ToNot(HaveOccurred()) Expect(f).ToNot(BeNil()) _, err = fs.FS.Stat(pluginFilePath) Expect(err).ToNot(HaveOccurred()) args = []string{"--domain", "example.com"} }) AfterEach(func() { filename := filepath.Join("tmp", "externalPlugin", "LICENSE") var fileInfo os.FileInfo fileInfo, err = fs.FS.Stat(filename) Expect(err).ToNot(HaveOccurred()) Expect(fileInfo).NotTo(BeNil()) }) It("should successfully run init subcommand on the external plugin", func() { i := initSubcommand{ Path: pluginFileName, Args: args, } err = i.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) }) It("should successfully run edit subcommand on the external plugin", func() { e := editSubcommand{ Path: pluginFileName, Args: args, } err = e.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) }) It("should successfully run create api subcommand on the external plugin", func() { c := createAPISubcommand{ Path: pluginFileName, Args: args, } err = c.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) }) It("should successfully run create webhook subcommand on the external plugin", func() { c := createWebhookSubcommand{ Path: pluginFileName, Args: args, } err = c.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) }) }) Context("with invalid mock values of GetExecOutput() and GetCurrentDir()", func() { var ( pluginFileName string args []string fs machinery.Filesystem err error ) BeforeEach(func() { outputGetter = &mockInValidOutputGetter{} currentDirGetter = &mockValidOsWdGetter{} fs = machinery.Filesystem{ FS: afero.NewMemMapFs(), } pluginFileName = externalPlugin args = []string{"--domain", "example.com"} }) It("should return error upon running init subcommand on the external plugin", func() { i := initSubcommand{ Path: pluginFileName, Args: args, } err = i.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting exec command output")) outputGetter = &mockValidOutputGetter{} currentDirGetter = &mockInValidOsWdGetter{} err = i.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting current directory")) }) It("should return error upon running edit subcommand on the external plugin", func() { e := editSubcommand{ Path: pluginFileName, Args: args, } err = e.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting exec command output")) outputGetter = &mockValidOutputGetter{} currentDirGetter = &mockInValidOsWdGetter{} err = e.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting current directory")) }) It("should return error upon running create api subcommand on the external plugin", func() { c := createAPISubcommand{ Path: pluginFileName, Args: args, } err = c.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting exec command output")) outputGetter = &mockValidOutputGetter{} currentDirGetter = &mockInValidOsWdGetter{} err = c.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting current directory")) }) It("should return error upon running create webhook subcommand on the external plugin", func() { c := createWebhookSubcommand{ Path: pluginFileName, Args: args, } err = c.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting exec command output")) outputGetter = &mockValidOutputGetter{} currentDirGetter = &mockInValidOsWdGetter{} err = c.Scaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("error getting current directory")) }) }) Context("with successfully getting flags from external plugin", func() { var ( pluginFileName string args []string flagset *pflag.FlagSet // Make an array of flags to represent the ones that should be returned in these tests flags []external.Flag checkFlagset func() ) BeforeEach(func() { outputGetter = &mockValidFlagOutputGetter{} currentDirGetter = &mockValidOsWdGetter{} pluginFileName = externalPlugin args = []string{"--captain", "black-beard", "--sail"} flagset = pflag.NewFlagSet("test", pflag.ContinueOnError) flags = getFlags() checkFlagset = func() { Expect(flagset.HasFlags()).To(BeTrue()) for _, flag := range flags { Expect(flagset.Lookup(flag.Name)).NotTo(BeNil()) // we parse floats as float64 Go type so this check will account for that if flag.Type != floatVal { Expect(flagset.Lookup(flag.Name).Value.Type()).To(Equal(flag.Type)) } else { Expect(flagset.Lookup(flag.Name).Value.Type()).To(Equal("float64")) } Expect(flagset.Lookup(flag.Name).Usage).To(Equal(flag.Usage)) Expect(flagset.Lookup(flag.Name).DefValue).To(Equal(flag.Default)) } } }) It("should successfully bind external plugin specified flags for `init` subcommand", func() { sc := initSubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) It("should successfully bind external plugin specified flags for `create api` subcommand", func() { sc := createAPISubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) It("should successfully bind external plugin specified flags for `create webhook` subcommand", func() { sc := createWebhookSubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) It("should successfully bind external plugin specified flags for `edit` subcommand", func() { sc := editSubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) }) Context("with failure to get flags from external plugin", func() { var ( pluginFileName string args []string flagset *pflag.FlagSet usage string checkFlagset func() ) BeforeEach(func() { outputGetter = &mockInValidOutputGetter{} currentDirGetter = &mockValidOsWdGetter{} pluginFileName = externalPlugin args = []string{"--captain", "black-beard", "--sail"} flagset = pflag.NewFlagSet("test", pflag.ContinueOnError) usage = "Kubebuilder could not validate this flag with the external plugin. " + "Consult the external plugin documentation for more information." checkFlagset = func() { Expect(flagset.HasFlags()).To(BeTrue()) Expect(flagset.Lookup("captain")).NotTo(BeNil()) Expect(flagset.Lookup("captain").Value.Type()).To(Equal("string")) Expect(flagset.Lookup("captain").Usage).To(Equal(usage)) Expect(flagset.Lookup("sail")).NotTo(BeNil()) Expect(flagset.Lookup("sail").Value.Type()).To(Equal("bool")) Expect(flagset.Lookup("sail").Usage).To(Equal(usage)) } }) It("should successfully bind all user passed flags for `init` subcommand", func() { sc := initSubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) It("should successfully bind all user passed flags for `create api` subcommand", func() { sc := createAPISubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) It("should successfully bind all user passed flags for `create webhook` subcommand", func() { sc := createWebhookSubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) It("should successfully bind all user passed flags for `edit` subcommand", func() { sc := editSubcommand{ Path: pluginFileName, Args: args, } sc.BindFlags(flagset) checkFlagset() }) }) Context("Flag Parsing Filter Functions", func() { It("gvk(Arg/Flag)Filter should filter out (--)group, (--)version, (--)kind", func() { for _, toBeFiltered := range []string{ "group", "version", "kind", } { Expect(gvkArgFilter("--" + toBeFiltered)).To(BeFalse()) Expect(gvkArgFilter(toBeFiltered)).To(BeFalse()) Expect(gvkFlagFilter(external.Flag{Name: "--" + toBeFiltered})).To(BeFalse()) Expect(gvkFlagFilter(external.Flag{Name: "--" + toBeFiltered})).To(BeFalse()) } Expect(gvkArgFilter("somerandomflag")).To(BeTrue()) Expect(gvkFlagFilter(external.Flag{Name: "somerandomflag"})).To(BeTrue()) }) It("helpArgFilter should filter out (--)help", func() { Expect(helpArgFilter("--help")).To(BeFalse()) Expect(helpArgFilter("help")).To(BeFalse()) Expect(helpArgFilter("somerandomflag")).To(BeTrue()) Expect(helpFlagFilter(external.Flag{Name: "--help"})).To(BeFalse()) Expect(helpFlagFilter(external.Flag{Name: "help"})).To(BeFalse()) Expect(helpFlagFilter(external.Flag{Name: "somerandomflag"})).To(BeTrue()) }) }) Context("Flag Parsing Helper Functions", func() { var ( fs *pflag.FlagSet args []string forbidden []string flags []external.Flag argFilters []argFilterFunc externalFlagFilters []externalFlagFilterFunc ) BeforeEach(func() { args = []string{ "--domain", "something.com", "--boolean", "--another", "flag", "--help", "--group", "somegroup", "--kind", "somekind", "--version", "someversion", } forbidden = []string{ "help", "group", "kind", "version", } fs = pflag.NewFlagSet("test", pflag.ContinueOnError) flagsToAppend := getFlags() flags = make([]external.Flag, len(flagsToAppend)) copy(flags, flagsToAppend) argFilters = []argFilterFunc{ gvkArgFilter, helpArgFilter, } externalFlagFilters = []externalFlagFilterFunc{ gvkFlagFilter, helpFlagFilter, } }) It("isBooleanFlag should return true if boolean flag provided at index", func() { Expect(isBooleanFlag(2, args)).To(BeTrue()) }) It("isBooleanFlag should return false if boolean flag not provided at index", func() { Expect(isBooleanFlag(0, args)).To(BeFalse()) }) It("bindAllFlags should bind all flags", func() { usage := "Kubebuilder could not validate this flag with the external plugin. " + "Consult the external plugin documentation for more information." bindAllFlags(fs, filterArgs(args, argFilters)) Expect(fs.HasFlags()).To(BeTrue()) Expect(fs.Lookup("domain")).NotTo(BeNil()) Expect(fs.Lookup("domain").Value.Type()).To(Equal("string")) Expect(fs.Lookup("domain").Usage).To(Equal(usage)) Expect(fs.Lookup("boolean")).NotTo(BeNil()) Expect(fs.Lookup("boolean").Value.Type()).To(Equal("bool")) Expect(fs.Lookup("boolean").Usage).To(Equal(usage)) Expect(fs.Lookup("another")).NotTo(BeNil()) Expect(fs.Lookup("another").Value.Type()).To(Equal("string")) Expect(fs.Lookup("another").Usage).To(Equal(usage)) By("bindAllFlags not have bound any forbidden flag after filtering") for i := range forbidden { Expect(fs.Lookup(forbidden[i])).To(BeNil()) } }) It("bindSpecificFlags should bind all flags in given []Flag", func() { filteredFlags := filterFlags(flags, externalFlagFilters) bindSpecificFlags(fs, filteredFlags) Expect(fs.HasFlags()).To(BeTrue()) for _, flag := range filteredFlags { Expect(fs.Lookup(flag.Name)).NotTo(BeNil()) // we parse floats as float64 Go type so this check will account for that if flag.Type != floatVal { Expect(fs.Lookup(flag.Name).Value.Type()).To(Equal(flag.Type)) } else { Expect(fs.Lookup(flag.Name).Value.Type()).To(Equal("float64")) } Expect(fs.Lookup(flag.Name).Usage).To(Equal(flag.Usage)) Expect(fs.Lookup(flag.Name).DefValue).To(Equal(flag.Default)) } By("bindSpecificFlags not have bound any forbidden flag after filtering") for i := range forbidden { Expect(fs.Lookup(forbidden[i])).To(BeNil()) } }) }) // TODO(everettraven): Add tests for an external plugin setting the Metadata and Examples Context("Successfully retrieving metadata and examples from external plugin", func() { var ( pluginFileName string metadata *plugin.SubcommandMetadata checkMetadata func() ) BeforeEach(func() { outputGetter = &mockValidMEOutputGetter{} currentDirGetter = &mockValidOsWdGetter{} pluginFileName = externalPlugin metadata = &plugin.SubcommandMetadata{} checkMetadata = func() { Expect(metadata.Description).Should(Equal(getMetadata().Description)) Expect(metadata.Examples).Should(Equal(getMetadata().Examples)) } }) It("should use the external plugin's metadata and examples for `init` subcommand", func() { sc := initSubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) It("should use the external plugin's metadata and examples for `create api` subcommand", func() { sc := createAPISubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) It("should use the external plugin's metadata and examples for `create webhook` subcommand", func() { sc := createWebhookSubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) It("should use the external plugin's metadata and examples for `edit` subcommand", func() { sc := editSubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) }) Context("Failing to retrieve metadata and examples from external plugin", func() { var ( pluginFileName string metadata *plugin.SubcommandMetadata checkMetadata func() ) BeforeEach(func() { outputGetter = &mockInValidOutputGetter{} currentDirGetter = &mockValidOsWdGetter{} pluginFileName = externalPlugin metadata = &plugin.SubcommandMetadata{} checkMetadata = func() { Expect(metadata.Description).Should(Equal(fmt.Sprintf(defaultMetadataTemplate, "myexternalplugin"))) Expect(metadata.Examples).Should(BeEmpty()) } }) It("should use the default metadata and examples for `init` subcommand", func() { sc := initSubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) It("should use the default metadata and examples for `create api` subcommand", func() { sc := createAPISubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) It("should use the default metadata and examples for `create webhook` subcommand", func() { sc := createWebhookSubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) It("should use the default metadata and examples for `edit` subcommand", func() { sc := editSubcommand{ Path: pluginFileName, Args: nil, } sc.UpdateMetadata(plugin.CLIMetadata{}, metadata) checkMetadata() }) }) Context("Helper functions for Sending request to external plugin and parsing response", func() { It("getUniverseMap should return path to content mapping of all files in Filesystem", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } files := []struct { path string name string content string }{ { path: "./", name: "file", content: "level 0 file", }, { path: "dir/", name: "file", content: "level 1 file", }, { path: "dir/subdir", name: "file", content: "level 2 file", }, } // create files in Filesystem for _, file := range files { err := fs.FS.MkdirAll(file.path, 0o700) Expect(err).ToNot(HaveOccurred()) f, err := fs.FS.Create(filepath.Join(file.path, file.name)) Expect(err).ToNot(HaveOccurred()) _, err = f.Write([]byte(file.content)) Expect(err).ToNot(HaveOccurred()) err = f.Close() Expect(err).ToNot(HaveOccurred()) } universe, err := getUniverseMap(fs) Expect(err).ToNot(HaveOccurred()) Expect(universe).To(HaveLen(len(files))) for _, file := range files { content := universe[filepath.Join(file.path, file.name)] Expect(content).To(Equal(file.content)) } }) }) Context("plugin chain propagation", func() { for _, tc := range pluginChainTestCases { caseData := tc Context(caseData.name, func() { It("keeps the CLI-provided chain when config omits pluginChain", func() { sub := caseData.new() cliChain := []string{"cli.plugin/v1"} sub.SetPluginChain(cliChain) cliChain[0] = "mutated" Expect(caseData.get(sub)).To(Equal([]string{"cli.plugin/v1"})) cfg := v3.New() Expect(sub.InjectConfig(cfg)).To(Succeed()) Expect(caseData.get(sub)).To(Equal([]string{"cli.plugin/v1"})) sub.SetPluginChain(nil) Expect(caseData.get(sub)).To(BeNil()) }) It("prefers the config plugin chain when present", func() { sub := caseData.new() sub.SetPluginChain([]string{"cli.plugin/v1"}) cfg := v3.New() expected := []string{"config.plugin/v2"} Expect(cfg.SetPluginChain(expected)).To(Succeed()) Expect(sub.InjectConfig(cfg)).To(Succeed()) Expect(caseData.get(sub)).To(Equal(expected)) }) }) } }) Context("PluginChain is passed to external plugin", func() { var ( pluginChainCaptured []string mockOutputGetter *mockPluginChainCaptureGetter ) BeforeEach(func() { mockOutputGetter = &mockPluginChainCaptureGetter{ capturedChain: &pluginChainCaptured, } outputGetter = mockOutputGetter currentDirGetter = &mockValidOsWdGetter{} }) It("should pass plugin chain to init subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } i := initSubcommand{ Path: "test.sh", Args: []string{"--domain", "example.com"}, pluginChain: []string{"go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"}, } err := i.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"})) }) It("should pass plugin chain to create api subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } c := createAPISubcommand{ Path: "test.sh", Args: []string{"--group", "apps", "--version", "v1", "--kind", "MyKind"}, pluginChain: []string{"go.kubebuilder.io/v4"}, } err := c.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4"})) }) It("should pass plugin chain to create webhook subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } w := createWebhookSubcommand{ Path: "test.sh", Args: []string{"--group", "apps", "--version", "v1", "--kind", "MyKind"}, pluginChain: []string{"go.kubebuilder.io/v3"}, } err := w.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v3"})) }) It("should pass plugin chain to edit subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } e := editSubcommand{ Path: "test.sh", Args: []string{"--multigroup"}, pluginChain: []string{"go.kubebuilder.io/v4", "declarative.go.kubebuilder.io/v1"}, } err := e.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4", "declarative.go.kubebuilder.io/v1"})) }) }) Context("with config injection", func() { const filePerm os.FileMode = 755 var ( pluginFileName string args []string f afero.File fs machinery.Filesystem mockGetter *mockConfigOutputGetter cfg *v3.Cfg expectedChain []string err error ) BeforeEach(func() { mockGetter = &mockConfigOutputGetter{} outputGetter = mockGetter currentDirGetter = &mockValidOsWdGetter{} fs = machinery.Filesystem{ FS: afero.NewMemMapFs(), } pluginFileName = "externalPlugin.sh" pluginFilePath := filepath.Join("tmp", "externalPlugin", pluginFileName) err = fs.FS.MkdirAll(filepath.Dir(pluginFilePath), filePerm) Expect(err).ToNot(HaveOccurred()) f, err = fs.FS.Create(pluginFilePath) Expect(err).ToNot(HaveOccurred()) Expect(f).ToNot(BeNil()) _, err = fs.FS.Stat(pluginFilePath) Expect(err).ToNot(HaveOccurred()) args = []string{"--domain", "example.com"} cfg = &v3.Cfg{ Version: v3.Version, Domain: "test.domain", Repository: "github.com/test/repo", Name: "test-project", } expectedChain = []string{"go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"} Expect(cfg.SetPluginChain(expectedChain)).To(Succeed()) }) It("should pass config to external plugin on init subcommand", func() { i := initSubcommand{ Path: pluginFileName, Args: args, } Expect(i.InjectConfig(cfg)).To(Succeed()) err = i.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(mockGetter.capturedRequest).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain")) Expect(mockGetter.capturedRequest.Config["repo"]).To(Equal("github.com/test/repo")) Expect(mockGetter.capturedRequest.Config["projectName"]).To(Equal("test-project")) Expect(mockGetter.capturedRequest.PluginChain).To(Equal(expectedChain)) }) It("should pass config to external plugin on create api subcommand", func() { c := createAPISubcommand{ Path: pluginFileName, Args: args, } Expect(c.InjectConfig(cfg)).To(Succeed()) err = c.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(mockGetter.capturedRequest).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain")) Expect(mockGetter.capturedRequest.PluginChain).To(Equal(expectedChain)) }) It("should pass config to external plugin on create webhook subcommand", func() { c := createWebhookSubcommand{ Path: pluginFileName, Args: args, } Expect(c.InjectConfig(cfg)).To(Succeed()) err = c.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(mockGetter.capturedRequest).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain")) Expect(mockGetter.capturedRequest.PluginChain).To(Equal(expectedChain)) }) It("should pass config to external plugin on edit subcommand", func() { e := editSubcommand{ Path: pluginFileName, Args: args, } Expect(e.InjectConfig(cfg)).To(Succeed()) err = e.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(mockGetter.capturedRequest).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config["domain"]).To(Equal("test.domain")) Expect(mockGetter.capturedRequest.PluginChain).To(Equal(expectedChain)) }) It("should handle nil config gracefully", func() { i := initSubcommand{ Path: pluginFileName, Args: args, } Expect(i.InjectConfig(nil)).To(Succeed()) err = i.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(mockGetter.capturedRequest).ToNot(BeNil()) Expect(mockGetter.capturedRequest.Config).To(BeNil()) Expect(mockGetter.capturedRequest.PluginChain).To(BeNil()) }) }) Context("PluginChain is passed to external plugin", func() { var ( pluginChainCaptured []string mockOutputGetter *mockPluginChainCaptureGetter ) BeforeEach(func() { pluginChainCaptured = nil mockOutputGetter = &mockPluginChainCaptureGetter{ capturedChain: &pluginChainCaptured, } outputGetter = mockOutputGetter currentDirGetter = &mockValidOsWdGetter{} }) It("should pass plugin chain to init subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } cfg := &v3.Cfg{Version: v3.Version} Expect(cfg.SetPluginChain([]string{"go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"})).To(Succeed()) i := initSubcommand{ Path: "test.sh", Args: []string{"--domain", "example.com"}, } Expect(i.InjectConfig(cfg)).To(Succeed()) err := i.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2"})) }) It("should pass plugin chain to create api subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } cfg := &v3.Cfg{Version: v3.Version} Expect(cfg.SetPluginChain([]string{"go.kubebuilder.io/v4"})).To(Succeed()) c := createAPISubcommand{ Path: "test.sh", Args: []string{"--group", "apps", "--version", "v1", "--kind", "MyKind"}, } Expect(c.InjectConfig(cfg)).To(Succeed()) err := c.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4"})) }) It("should pass plugin chain to create webhook subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } cfg := &v3.Cfg{Version: v3.Version} Expect(cfg.SetPluginChain([]string{"go.kubebuilder.io/v3"})).To(Succeed()) w := createWebhookSubcommand{ Path: "test.sh", Args: []string{"--group", "apps", "--version", "v1", "--kind", "MyKind"}, } Expect(w.InjectConfig(cfg)).To(Succeed()) err := w.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v3"})) }) It("should pass plugin chain to edit subcommand", func() { fs := machinery.Filesystem{ FS: afero.NewMemMapFs(), } cfg := &v3.Cfg{Version: v3.Version} Expect(cfg.SetPluginChain([]string{"go.kubebuilder.io/v4", "declarative.go.kubebuilder.io/v1"})).To(Succeed()) e := editSubcommand{ Path: "test.sh", Args: []string{"--multigroup"}, } Expect(e.InjectConfig(cfg)).To(Succeed()) err := e.Scaffold(fs) Expect(err).ToNot(HaveOccurred()) Expect(pluginChainCaptured).To(Equal([]string{"go.kubebuilder.io/v4", "declarative.go.kubebuilder.io/v1"})) }) }) }) func getFlags() []external.Flag { return []external.Flag{ { Name: "captain", Type: "string", Usage: "specify the ship captain", Default: "jack-sparrow", }, { Name: "sail", Type: "bool", Usage: "deploy the sail", Default: "false", }, { Name: "crew-count", Type: "int", Usage: "number of crew members", Default: "123", }, { Name: "treasure-value", Type: "float", Usage: "value of treasure on board the ship", Default: "123.45", }, } } func getMetadata() plugin.SubcommandMetadata { return plugin.SubcommandMetadata{ Description: "Test description", Examples: "Test examples", } } var _ = Describe("Plugin", func() { var p Plugin BeforeEach(func() { p = Plugin{ PName: "testplugin", PVersion: plugin.Version{Number: 1}, PSupportedProjectVersions: []config.Version{ {Number: 3}, }, Path: "/path/to/plugin", Args: []string{"--flag", "value"}, } }) It("should return the plugin name", func() { Expect(p.Name()).To(Equal("testplugin")) }) It("should return the plugin version", func() { Expect(p.Version()).To(Equal(plugin.Version{Number: 1})) }) It("should return supported project versions", func() { Expect(p.SupportedProjectVersions()).To(Equal([]config.Version{{Number: 3}})) }) It("should return init subcommand", func() { sub := p.GetInitSubcommand() Expect(sub).NotTo(BeNil()) initSub, ok := sub.(*initSubcommand) Expect(ok).To(BeTrue()) Expect(initSub.Path).To(Equal("/path/to/plugin")) Expect(initSub.Args).To(Equal([]string{"--flag", "value"})) }) It("should return create API subcommand", func() { sub := p.GetCreateAPISubcommand() Expect(sub).NotTo(BeNil()) apiSub, ok := sub.(*createAPISubcommand) Expect(ok).To(BeTrue()) Expect(apiSub.Path).To(Equal("/path/to/plugin")) Expect(apiSub.Args).To(Equal([]string{"--flag", "value"})) }) It("should return create webhook subcommand", func() { sub := p.GetCreateWebhookSubcommand() Expect(sub).NotTo(BeNil()) webhookSub, ok := sub.(*createWebhookSubcommand) Expect(ok).To(BeTrue()) Expect(webhookSub.Path).To(Equal("/path/to/plugin")) Expect(webhookSub.Args).To(Equal([]string{"--flag", "value"})) }) It("should return edit subcommand", func() { sub := p.GetEditSubcommand() Expect(sub).NotTo(BeNil()) editSub, ok := sub.(*editSubcommand) Expect(ok).To(BeTrue()) Expect(editSub.Path).To(Equal("/path/to/plugin")) Expect(editSub.Args).To(Equal([]string{"--flag", "value"})) }) It("should return empty deprecation warning", func() { Expect(p.DeprecationWarning()).To(BeEmpty()) }) }) ================================================ FILE: pkg/plugins/external/helpers.go ================================================ /* Copyright 2021 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 external import ( "bytes" "encoding/json" "fmt" "io" iofs "io/fs" "os" "os/exec" "path/filepath" "slices" "strconv" "strings" "github.com/spf13/afero" "github.com/spf13/pflag" "sigs.k8s.io/yaml" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" ) var outputGetter ExecOutputGetter = &execOutputGetter{} const defaultMetadataTemplate = ` %s is an external plugin for scaffolding files to help with your Operator development. For more information on how to use this external plugin, it is recommended to consult the external plugin's documentation. ` // ExecOutputGetter is an interface that implements the exec output method. type ExecOutputGetter interface { GetExecOutput(req []byte, path string) ([]byte, error) } type execOutputGetter struct{} func (e *execOutputGetter) GetExecOutput(request []byte, path string) ([]byte, error) { cmd := exec.Command(path) //nolint:gosec cmd.Stdin = bytes.NewBuffer(request) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { return nil, fmt.Errorf("error getting output for cmd %q: %w", cmd, err) } return out, nil } var currentDirGetter OsWdGetter = &osWdGetter{} // OsWdGetter is an interface that implements the get current directory method. type OsWdGetter interface { GetCurrentDir() (string, error) } type osWdGetter struct{} func (o *osWdGetter) GetCurrentDir() (string, error) { currentDir, err := os.Getwd() if err != nil { return "", fmt.Errorf("error getting current directory: %w", err) } return currentDir, nil } func makePluginRequest(req external.PluginRequest, path string) (*external.PluginResponse, error) { reqBytes, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("error marshalling plugin request: %w", err) } out, err := outputGetter.GetExecOutput(reqBytes, path) if err != nil { return nil, fmt.Errorf("error executing plugin request: %w", err) } res := external.PluginResponse{} if err = json.Unmarshal(out, &res); err != nil { return nil, fmt.Errorf("error unmarshalling plugin response: %w", err) } // Error if the plugin failed. if res.Error { return nil, fmt.Errorf("%s", strings.Join(res.ErrorMsgs, "\n")) } return &res, nil } // getUniverseMap is a helper function that is used to read the current directory to build // the universe map. // It will return a map[string]string where the keys are relative paths to files in the directory // and values are the contents, or an error if an issue occurred while reading one of the files. func getUniverseMap(fs machinery.Filesystem) (map[string]string, error) { universe := map[string]string{} err := afero.Walk(fs.FS, ".", func(path string, info iofs.FileInfo, err error) error { if err != nil { return fmt.Errorf("error walking path %q: %w", path, err) } if info.IsDir() { return nil } file, err := fs.FS.Open(path) if err != nil { return fmt.Errorf("error opening file %q: %w", path, err) } defer func() { if err = file.Close(); err != nil { return } }() content, err := io.ReadAll(file) if err != nil { return fmt.Errorf("error reading file %q: %w", path, err) } universe[path] = string(content) return nil }) if err != nil { return nil, fmt.Errorf("error walking the directory: %w", err) } return universe, nil } func handlePluginResponse(fs machinery.Filesystem, req external.PluginRequest, path string, cfg config.Config) error { var err error req.Universe, err = getUniverseMap(fs) if err != nil { return fmt.Errorf("error getting universe map: %w", err) } // Marshal config to include in the request if config is provided if cfg != nil { var configData []byte configData, err = cfg.MarshalYAML() if err != nil { return fmt.Errorf("error marshaling config: %w", err) } var configMap map[string]any if err = yaml.Unmarshal(configData, &configMap); err != nil { return fmt.Errorf("error unmarshaling config to map: %w", err) } req.Config = configMap } res, err := makePluginRequest(req, path) if err != nil { return fmt.Errorf("error making request to external plugin: %w", err) } currentDir, err := currentDirGetter.GetCurrentDir() if err != nil { return fmt.Errorf("error getting current directory: %w", err) } for filename, data := range res.Universe { file := filepath.Join(currentDir, filename) dir := filepath.Dir(file) // create the directory if it does not exist if err = os.MkdirAll(dir, 0o750); err != nil { return fmt.Errorf("error creating the directory: %w", err) } f, createErr := fs.FS.Create(file) if createErr != nil { return fmt.Errorf("error creating file %q: %w", file, createErr) } defer func() { if err = f.Close(); err != nil { return } }() if _, err = f.Write([]byte(data)); err != nil { return fmt.Errorf("error writing file %q: %w", file, err) } } return nil } // getExternalPluginFlags is a helper function that is used to get a list of flags from an external plugin. // It will return []Flag if successful or an error if there is an issue attempting to get the list of flags. func getExternalPluginFlags(req external.PluginRequest, path string) ([]external.Flag, error) { req.Universe = map[string]string{} res, err := makePluginRequest(req, path) if err != nil { return nil, fmt.Errorf("error making request to external plugin: %w", err) } return res.Flags, nil } // isBooleanFlag is a helper function to determine if an argument flag is a boolean flag func isBooleanFlag(argIndex int, args []string) bool { return argIndex+1 < len(args) && strings.Contains(args[argIndex+1], "--") || argIndex+1 >= len(args) } // bindAllFlags will bind all flags passed into the subcommand by a user func bindAllFlags(fs *pflag.FlagSet, args []string) { defaultFlagDescription := "Kubebuilder could not validate this flag with the external plugin. " + "Consult the external plugin documentation for more information." // Bind all flags passed in for i := range args { if strings.Contains(args[i], "--") { flag := strings.Replace(args[i], "--", "", 1) // Check if the flag is a boolean flag if isBooleanFlag(i, args) { _ = fs.Bool(flag, false, defaultFlagDescription) } else { _ = fs.String(flag, "", defaultFlagDescription) } } } } // bindSpecificFlags binds flags that are specified by an external plugin as allowed. func bindSpecificFlags(fs *pflag.FlagSet, flags []external.Flag) { // Only bind flags returned by the external plugin for _, flag := range flags { switch flag.Type { case "bool": defaultValue, _ := strconv.ParseBool(flag.Default) _ = fs.Bool(flag.Name, defaultValue, flag.Usage) case "int": defaultValue, _ := strconv.Atoi(flag.Default) _ = fs.Int(flag.Name, defaultValue, flag.Usage) case "float": defaultValue, _ := strconv.ParseFloat(flag.Default, 64) _ = fs.Float64(flag.Name, defaultValue, flag.Usage) default: _ = fs.String(flag.Name, flag.Default, flag.Usage) } } } func filterFlags(flags []external.Flag, externalFlagFilters []externalFlagFilterFunc) []external.Flag { var filteredFlags []external.Flag for _, flag := range flags { ok := true for _, filter := range externalFlagFilters { if !filter(flag) { ok = false break } } if ok { filteredFlags = append(filteredFlags, flag) } } return filteredFlags } func filterArgs(args []string, argFilters []argFilterFunc) []string { var filteredArgs []string for _, arg := range args { ok := true for _, filter := range argFilters { if !filter(arg) { ok = false break } } if ok { filteredArgs = append(filteredArgs, arg) } } return filteredArgs } type ( externalFlagFilterFunc func(flag external.Flag) bool argFilterFunc func(arg string) bool ) var ( // see gvkArgFilter gvkFlagFilter = func(flag external.Flag) bool { return gvkArgFilter(flag.Name) } // gvkFlagFilter filters out any flag named "group", "version", "kind" as // they are already bound by kubebuilder gvkArgFilter = func(arg string) bool { arg = strings.Replace(arg, "--", "", 1) return !slices.Contains([]string{ "group", "version", "kind", }, arg) } // see helpArgFilter helpFlagFilter = func(flag external.Flag) bool { return helpArgFilter(flag.Name) } // helpArgFilter filters out any flag named "help" as its already bound helpArgFilter = func(arg string) bool { arg = strings.Replace(arg, "--", "", 1) return arg != "help" } ) func bindExternalPluginFlags(fs *pflag.FlagSet, subcommand string, path string, args []string) { req := external.PluginRequest{ APIVersion: defaultAPIVersion, Command: "flags", Args: []string{"--" + subcommand}, } // Get a list of flags for the init subcommand of the external plugin // If it returns an error, parse all flags passed by the user and let // the external plugin return an unknown flag error. flags, err := getExternalPluginFlags(req, path) // Filter Flags based on a set of filters that we do not want. // can be used to filter out non-overridable flags or other // criteria by creating your own filterFlagFunc if err != nil { bindAllFlags(fs, filterArgs(args, []argFilterFunc{ gvkArgFilter, helpArgFilter, })) } else { bindSpecificFlags(fs, filterFlags(flags, []externalFlagFilterFunc{ gvkFlagFilter, helpFlagFilter, })) } } // setExternalPluginMetadata is a helper function that sets the subcommand // metadata that is used when the help text is shown for a subcommand. // It will attempt to get the Metadata from the external plugin. If the // external plugin returns no Metadata or an error, a default will be used. func setExternalPluginMetadata(subcommand, path string, subcmdMeta *plugin.SubcommandMetadata) { fileName := filepath.Base(path) subcmdMeta.Description = fmt.Sprintf(defaultMetadataTemplate, fileName[:len(fileName)-len(filepath.Ext(fileName))]) res, _ := getExternalPluginMetadata(subcommand, path) if res != nil { if res.Description != "" { subcmdMeta.Description = res.Description } if res.Examples != "" { subcmdMeta.Examples = res.Examples } } } // fetchExternalPluginMetadata performs the actual request to the // external plugin to get the metadata. It returns the metadata // or an error if an error occurs during the fetch process. func getExternalPluginMetadata(subcommand, path string) (*plugin.SubcommandMetadata, error) { req := external.PluginRequest{ APIVersion: defaultAPIVersion, Command: "metadata", Args: []string{"--" + subcommand}, Universe: map[string]string{}, } res, err := makePluginRequest(req, path) if err != nil { return nil, fmt.Errorf("error making request to external plugin: %w", err) } return &res.Metadata, nil } ================================================ FILE: pkg/plugins/external/helpers_test.go ================================================ /* Copyright 2026 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 external import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" ) var _ = Describe("helpers", func() { Context("isBooleanFlag", func() { It("should return true when next arg starts with --", func() { args := []string{"--flag1", "--flag2", "value"} Expect(isBooleanFlag(0, args)).To(BeTrue()) }) It("should return true when at end of args", func() { args := []string{"--flag1", "--flag2"} Expect(isBooleanFlag(1, args)).To(BeTrue()) }) It("should return false when next arg is a value", func() { args := []string{"--flag1", "value", "--flag2"} Expect(isBooleanFlag(0, args)).To(BeFalse()) }) It("should return true for last argument", func() { args := []string{"--flag"} Expect(isBooleanFlag(0, args)).To(BeTrue()) }) }) Context("filterFlags", func() { var testFlags []external.Flag BeforeEach(func() { testFlags = []external.Flag{ {Name: "group", Type: "string"}, {Name: "version", Type: "string"}, {Name: "kind", Type: "string"}, {Name: "custom", Type: "string"}, {Name: "help", Type: "bool"}, } }) It("should filter flags based on single filter", func() { filter := func(flag external.Flag) bool { return flag.Name != "group" } result := filterFlags(testFlags, []externalFlagFilterFunc{filter}) Expect(result).To(HaveLen(4)) for _, flag := range result { Expect(flag.Name).NotTo(Equal("group")) } }) It("should filter flags based on multiple filters", func() { filter1 := func(flag external.Flag) bool { return flag.Name != "group" } filter2 := func(flag external.Flag) bool { return flag.Name != "help" } result := filterFlags(testFlags, []externalFlagFilterFunc{filter1, filter2}) Expect(result).To(HaveLen(3)) Expect(result).To(ContainElement(external.Flag{Name: "version", Type: "string"})) Expect(result).To(ContainElement(external.Flag{Name: "kind", Type: "string"})) Expect(result).To(ContainElement(external.Flag{Name: "custom", Type: "string"})) }) It("should return empty when all flags filtered out", func() { filter := func(_ external.Flag) bool { return false } result := filterFlags(testFlags, []externalFlagFilterFunc{filter}) Expect(result).To(BeEmpty()) }) It("should return all flags when no filters reject", func() { filter := func(_ external.Flag) bool { return true } result := filterFlags(testFlags, []externalFlagFilterFunc{filter}) Expect(result).To(Equal(testFlags)) }) }) Context("filterArgs", func() { It("should filter args based on single filter", func() { args := []string{"--group", "--version", "--custom", "--help"} filter := func(arg string) bool { return arg != "--group" } result := filterArgs(args, []argFilterFunc{filter}) Expect(result).To(Equal([]string{"--version", "--custom", "--help"})) }) It("should filter args based on multiple filters", func() { args := []string{"--group", "--version", "--custom"} filter1 := func(arg string) bool { return arg != "--group" } filter2 := func(arg string) bool { return arg != "--version" } result := filterArgs(args, []argFilterFunc{filter1, filter2}) Expect(result).To(Equal([]string{"--custom"})) }) It("should return empty when all args filtered out", func() { args := []string{"--arg1", "--arg2"} filter := func(_ string) bool { return false } result := filterArgs(args, []argFilterFunc{filter}) Expect(result).To(BeEmpty()) }) It("should return all args when no filters reject", func() { args := []string{"--arg1", "--arg2"} filter := func(_ string) bool { return true } result := filterArgs(args, []argFilterFunc{filter}) Expect(result).To(Equal(args)) }) }) Context("gvkArgFilter", func() { It("should filter out group flag", func() { Expect(gvkArgFilter("group")).To(BeFalse()) Expect(gvkArgFilter("--group")).To(BeFalse()) }) It("should filter out version flag", func() { Expect(gvkArgFilter("version")).To(BeFalse()) Expect(gvkArgFilter("--version")).To(BeFalse()) }) It("should filter out kind flag", func() { Expect(gvkArgFilter("kind")).To(BeFalse()) Expect(gvkArgFilter("--kind")).To(BeFalse()) }) It("should allow other flags", func() { Expect(gvkArgFilter("custom")).To(BeTrue()) Expect(gvkArgFilter("--custom")).To(BeTrue()) Expect(gvkArgFilter("domain")).To(BeTrue()) }) }) Context("gvkFlagFilter", func() { It("should filter out group, version, kind flags", func() { Expect(gvkFlagFilter(external.Flag{Name: "group"})).To(BeFalse()) Expect(gvkFlagFilter(external.Flag{Name: "version"})).To(BeFalse()) Expect(gvkFlagFilter(external.Flag{Name: "kind"})).To(BeFalse()) }) It("should allow other flags", func() { Expect(gvkFlagFilter(external.Flag{Name: "custom"})).To(BeTrue()) Expect(gvkFlagFilter(external.Flag{Name: "domain"})).To(BeTrue()) }) }) Context("helpArgFilter", func() { It("should filter out help flag", func() { Expect(helpArgFilter("help")).To(BeFalse()) Expect(helpArgFilter("--help")).To(BeFalse()) }) It("should allow other flags", func() { Expect(helpArgFilter("custom")).To(BeTrue()) Expect(helpArgFilter("--custom")).To(BeTrue()) Expect(helpArgFilter("helpful")).To(BeTrue()) }) }) Context("helpFlagFilter", func() { It("should filter out help flag", func() { Expect(helpFlagFilter(external.Flag{Name: "help"})).To(BeFalse()) }) It("should allow other flags", func() { Expect(helpFlagFilter(external.Flag{Name: "custom"})).To(BeTrue()) Expect(helpFlagFilter(external.Flag{Name: "helpful"})).To(BeTrue()) }) }) }) ================================================ FILE: pkg/plugins/external/init.go ================================================ /* Copyright 2021 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. */ //nolint:dupl package external import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" ) var _ plugin.InitSubcommand = &initSubcommand{} type initSubcommand struct { Path string Args []string pluginChain []string config config.Config } // InjectConfig injects the project configuration to access plugin chain information func (p *initSubcommand) InjectConfig(c config.Config) error { p.config = c if c == nil { return nil } if chain := c.GetPluginChain(); len(chain) > 0 { p.pluginChain = append([]string(nil), chain...) } return nil } func (p *initSubcommand) SetPluginChain(chain []string) { if len(chain) == 0 { p.pluginChain = nil return } p.pluginChain = append([]string(nil), chain...) } func (p *initSubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { setExternalPluginMetadata("init", p.Path, subcmdMeta) } func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "init", p.Path, p.Args) } func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error { req := external.PluginRequest{ APIVersion: defaultAPIVersion, Command: "init", Args: p.Args, PluginChain: p.pluginChain, } err := handlePluginResponse(fs, req, p.Path, p.config) if err != nil { return err } return nil } ================================================ FILE: pkg/plugins/external/plugin.go ================================================ /* Copyright 2021 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 external import ( "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) var _ plugin.Full = Plugin{} // Plugin implements the plugin.Full interface type Plugin struct { PName string PVersion plugin.Version PSupportedProjectVersions []config.Version Path string Args []string } // Name returns the name of the plugin func (p Plugin) Name() string { return p.PName } // Version returns the version of the plugin func (p Plugin) Version() plugin.Version { return p.PVersion } // SupportedProjectVersions returns an array with all project versions supported by the plugin func (p Plugin) SupportedProjectVersions() []config.Version { return p.PSupportedProjectVersions } // GetInitSubcommand will return the subcommand which is responsible for initializing and common scaffolding func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &initSubcommand{ Path: p.Path, Args: p.Args, } } // GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &createAPISubcommand{ Path: p.Path, Args: p.Args, } } // GetCreateWebhookSubcommand will return the subcommand which is responsible for scaffolding webhooks func (p Plugin) GetCreateWebhookSubcommand() plugin.CreateWebhookSubcommand { return &createWebhookSubcommand{ Path: p.Path, Args: p.Args, } } // GetEditSubcommand will return the subcommand which is responsible for editing the scaffold of the project func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &editSubcommand{ Path: p.Path, Args: p.Args, } } // DeprecationWarning define the deprecation message or return empty when plugin is not deprecated func (p Plugin) DeprecationWarning() string { return "" } ================================================ FILE: pkg/plugins/external/webhook.go ================================================ /* Copyright 2021 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 external import ( "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/external" ) var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{} type createWebhookSubcommand struct { Path string Args []string pluginChain []string config config.Config } // InjectConfig injects the project configuration so external plugins can read the PROJECT file. func (p *createWebhookSubcommand) InjectConfig(c config.Config) error { p.config = c if c == nil { return nil } if chain := c.GetPluginChain(); len(chain) > 0 { p.pluginChain = append([]string(nil), chain...) } return nil } func (p *createWebhookSubcommand) SetPluginChain(chain []string) { if len(chain) == 0 { p.pluginChain = nil return } p.pluginChain = append([]string(nil), chain...) } func (p *createWebhookSubcommand) InjectResource(*resource.Resource) error { // Do nothing since resource flags are passed to the external plugin directly. return nil } func (p *createWebhookSubcommand) UpdateMetadata(_ plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { setExternalPluginMetadata("webhook", p.Path, subcmdMeta) } func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) { bindExternalPluginFlags(fs, "webhook", p.Path, p.Args) } func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error { req := external.PluginRequest{ APIVersion: defaultAPIVersion, Command: "create webhook", Args: p.Args, PluginChain: p.pluginChain, } err := handlePluginResponse(fs, req, p.Path, p.config) if err != nil { return err } return nil } ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/api.go ================================================ /* Copyright 2022 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 v1alpha1 import ( "errors" "fmt" log "log/slog" "os" "strings" "github.com/spf13/pflag" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds" ) var _ plugin.CreateAPISubcommand = &createAPISubcommand{} type createAPISubcommand struct { config config.Config options *goPlugin.Options resource *resource.Resource // image indicates the image that will be used to scaffold the deployment image string // runMake indicates whether to run make or not after scaffolding APIs runMake bool // runManifests indicates whether to run manifests or not after scaffolding APIs runManifests bool // imageCommand indicates the command that we should use to init the deployment imageContainerCommand string // imageContainerPort indicates the port that we should use in the scaffold imageContainerPort string // runAsUser indicates the user-id used for running the container runAsUser string } func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) { //nolint:lll subcmdMeta.Description = `Scaffold the code implementation to deploy and manage your Operand which is represented by the API informed and will be reconciled by its controller. This plugin will generate the code implementation to help you out. Note: In general, it’s recommended to have one controller responsible for managing each API created for the project to properly follow the design goals set by Controller Runtime(https://github.com/kubernetes-sigs/controller-runtime). This plugin will work as the common behaviour of the flag --force and will scaffold the API and controller always. Use core types or external APIs is not officially support by default with. ` //nolint:lll subcmdMeta.Examples = fmt.Sprintf(` # Create a frigates API with Group: ship, Version: v1beta1, Kind: Frigate to represent the Image: example.com/frigate:v0.0.1 and its controller with a code to deploy and manage this Operand. Note that in the following example we are also adding the optional options to let you inform the command which should be used to create the container and initialize itvia the flag --image-container-command as the Port that should be used - By informing the command (--image-container-command="memcached,--memory-limit=64,-o,modern,-v") your deployment will be scaffold with, i.e.: Command: []string{"memcached","--memory-limit=64","-o","modern","-v"}, - By informing the Port (--image-container-port) will deployment will be scaffold with, i.e: Ports: []corev1.ContainerPort{ ContainerPort: Memcached.Spec.ContainerPort, Name: "Memcached", }, Therefore, the default values informed will be used to scaffold specs for the API. %[1]s create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:1.6.15-alpine --image-container-command="memcached --memory-limit=64 modern -v" --image-container-port="11211" --plugins="%[2]s" --make=false --namespaced=false # Generate the manifests make manifests # Install CRDs into the Kubernetes cluster using kubectl apply make install # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config make run `, cliMeta.CommandName, plugin.KeyFor(Plugin{})) } func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.StringVar(&p.image, "image", "", "inform the Operand image. "+ "The controller will be scaffolded with an example code to deploy and manage this image.") fs.StringVar(&p.imageContainerCommand, "image-container-command", "", "[Optional] if informed, "+ "will be used to scaffold the container command that should be used to init a container to run the image in "+ "the controller and its spec in the API (CRD/CR). (i.e. "+ "--image-container-command=\"memcached,--memory-limit=64,modern,-o,-v\")") fs.StringVar(&p.imageContainerPort, "image-container-port", "", "[Optional] if informed, "+ "will be used to scaffold the container port that should be used by container image in "+ "the controller and its spec in the API (CRD/CR). (i.e --image-container-port=\"11211\") ") fs.StringVar(&p.runAsUser, "run-as-user", "", "User-Id for the container formed will be set to this value") fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files") fs.BoolVar(&p.runManifests, "manifests", true, "if true, run `make manifests` after generating files") p.options = &goPlugin.Options{} fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form") } func (p *createAPISubcommand) InjectConfig(c config.Config) error { p.config = c return nil } func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { p.resource = res p.options.DoAPI = true p.options.DoController = true p.options.Namespaced = true p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { return fmt.Errorf("error validating resource: %w", err) } // Check that the provided group can be added to the project if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) { return fmt.Errorf("multiple groups are not allowed by default, " + "to enable multi-group visit https://kubebuilder.io/migration/multi-group.html") } return nil } func (p *createAPISubcommand) PreScaffold(machinery.Filesystem) error { if len(p.image) == 0 { return fmt.Errorf("you MUST inform the image that will be used in the reconciliation") } isGoV3 := false for _, pluginKey := range p.config.GetPluginChain() { if strings.Contains(pluginKey, "go.kubebuilder.io/v3") { isGoV3 = true } } defaultMainPath := "cmd/main.go" if isGoV3 { defaultMainPath = "main.go" } // check if main.go is present in the cmd/ directory if _, err := os.Stat(defaultMainPath); os.IsNotExist(err) { return fmt.Errorf("main.go file should be present in %s", defaultMainPath) } return nil } func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error { log.Info("updating scaffold with deploy-image/v1alpha1 plugin...") scaffolder := scaffolds.NewDeployImageScaffolder(p.config, *p.resource, p.image, p.imageContainerCommand, p.imageContainerPort, p.runAsUser) scaffolder.InjectFS(fs) err := scaffolder.Scaffold() if err != nil { return fmt.Errorf("error scaffolding deploy-image plugin: %w", err) } // Save resource info to PROJECT file key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{}) canonicalKey := plugin.KeyFor(Plugin{}) cfg := PluginConfig{} if err = p.config.DecodePluginConfig(key, &cfg); err != nil { switch { case errors.As(err, &config.UnsupportedFieldError{}): // Config version doesn't support plugin metadata return nil case errors.As(err, &config.PluginKeyNotFoundError{}): if key != canonicalKey { if decodeErr := p.config.DecodePluginConfig(canonicalKey, &cfg); decodeErr != nil { if errors.As(decodeErr, &config.UnsupportedFieldError{}) { return nil } if !errors.As(decodeErr, &config.PluginKeyNotFoundError{}) { return fmt.Errorf("error decoding plugin configuration: %w", decodeErr) } } } default: return fmt.Errorf("error decoding plugin configuration: %w", err) } } configDataOptions := options{ Image: p.image, ContainerCommand: p.imageContainerCommand, ContainerPort: p.imageContainerPort, RunAsUser: p.runAsUser, } cfg.Resources = append(cfg.Resources, ResourceData{ Group: p.resource.Group, Domain: p.resource.Domain, Version: p.resource.Version, Kind: p.resource.Kind, Options: configDataOptions, }) if err = p.config.EncodePluginConfig(key, cfg); err != nil { return fmt.Errorf("error encoding plugin configuration: %w", err) } return nil } func (p *createAPISubcommand) PostScaffold() error { err := util.RunCmd("Update dependencies", "go", "mod", "tidy") if err != nil { return fmt.Errorf("error updating go dependencies: %w", err) } if p.runMake && p.resource.HasAPI() { err = util.RunCmd("Running make", "make", "generate") if err != nil { return fmt.Errorf("ailed running make generate: %w", err) } } if p.runManifests && p.resource.HasAPI() { err = util.RunCmd("Running make", "make", "manifests") if err != nil { return fmt.Errorf("failed running make manifests: %w", err) } } fmt.Print("Next: check the implementation of your new API and controller. " + "If you do changes in the API run the manifests with:\n$ make manifests\n") return nil } ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/api_test.go ================================================ /* Copyright 2026 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 v1alpha1 import ( "os" "path/filepath" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/spf13/afero" "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang" ) var _ = Describe("createAPISubcommand", func() { var ( subCmd *createAPISubcommand cfg config.Config res *resource.Resource fs machinery.Filesystem ) BeforeEach(func() { subCmd = &createAPISubcommand{} cfg = cfgv3.New() _ = cfg.SetRepository("github.com/example/test") subCmd.options = &goPlugin.Options{} res = &resource.Resource{ GVK: resource.GVK{ Group: "example.com", Domain: "test.io", Version: "v1alpha1", Kind: "Memcached", }, Plural: "memcacheds", API: &resource.API{}, Webhooks: &resource.Webhooks{}, } fs = machinery.Filesystem{FS: afero.NewMemMapFs()} Expect(subCmd.InjectConfig(cfg)).To(Succeed()) }) Context("PreScaffold validation", func() { It("should require image flag to be set", func() { subCmd.image = "" err := subCmd.PreScaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("you MUST inform the image")) }) It("should succeed when image is provided", func() { subCmd.image = "memcached:1.6.15-alpine" tmpDir, err := os.MkdirTemp("", "deploy-image-test") Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.RemoveAll(tmpDir) }() originalDir, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.Chdir(originalDir) }() err = os.Chdir(tmpDir) Expect(err).NotTo(HaveOccurred()) err = os.MkdirAll("cmd", 0o755) Expect(err).NotTo(HaveOccurred()) err = os.WriteFile(filepath.Join("cmd", "main.go"), []byte("package main"), 0o644) Expect(err).NotTo(HaveOccurred()) err = subCmd.PreScaffold(fs) Expect(err).NotTo(HaveOccurred()) }) It("should check for cmd/main.go in go/v4 projects", func() { subCmd.image = "busybox:1.36.1" _ = cfg.SetPluginChain([]string{"go.kubebuilder.io/v4"}) tmpDir, err := os.MkdirTemp("", "deploy-image-test") Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.RemoveAll(tmpDir) }() originalDir, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.Chdir(originalDir) }() err = os.Chdir(tmpDir) Expect(err).NotTo(HaveOccurred()) err = subCmd.PreScaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("main.go file should be present in cmd/main.go")) }) It("should check for main.go in go/v3 projects", func() { subCmd.image = "busybox:1.36.1" _ = cfg.SetPluginChain([]string{"go.kubebuilder.io/v3"}) tmpDir, err := os.MkdirTemp("", "deploy-image-test") Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.RemoveAll(tmpDir) }() originalDir, err := os.Getwd() Expect(err).NotTo(HaveOccurred()) defer func() { _ = os.Chdir(originalDir) }() err = os.Chdir(tmpDir) Expect(err).NotTo(HaveOccurred()) err = subCmd.PreScaffold(fs) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("main.go file should be present in main.go")) }) }) Context("InjectResource validation", func() { It("should set API and controller flags automatically", func() { err := subCmd.InjectResource(res) Expect(err).NotTo(HaveOccurred()) Expect(subCmd.options.DoAPI).To(BeTrue()) Expect(subCmd.options.DoController).To(BeTrue()) Expect(subCmd.options.Namespaced).To(BeTrue()) }) It("should prevent multiple groups in single-group project", func() { firstRes := resource.Resource{ GVK: resource.GVK{ Group: "ship", Domain: "test.io", Version: "v1", Kind: "Frigate", }, Plural: "frigates", API: &resource.API{CRDVersion: "v1"}, } Expect(cfg.AddResource(firstRes)).To(Succeed()) res.Group = "example.com" res.Plural = "memcacheds" err := subCmd.InjectResource(res) Expect(err).To(HaveOccurred()) Expect(err.Error()).To(ContainSubstring("multiple groups are not allowed")) }) It("should allow multiple groups when multigroup is enabled", func() { Expect(cfg.SetMultiGroup()).To(Succeed()) firstRes := resource.Resource{ GVK: resource.GVK{ Group: "ship", Domain: "test.io", Version: "v1", Kind: "Frigate", }, Plural: "frigates", API: &resource.API{CRDVersion: "v1"}, } Expect(cfg.AddResource(firstRes)).To(Succeed()) res.Group = "example.com" Expect(subCmd.InjectResource(res)).To(Succeed()) }) }) }) ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/plugin.go ================================================ /* Copyright 2022 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 v1alpha1 import ( "sigs.k8s.io/kubebuilder/v4/pkg/config" cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3" "sigs.k8s.io/kubebuilder/v4/pkg/model/stage" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang" ) const pluginName = "deploy-image." + golang.DefaultNameQualifier var ( pluginVersion = plugin.Version{Number: 1, Stage: stage.Alpha} supportedProjectVersions = []config.Version{cfgv3.Version} ) var _ plugin.CreateAPI = Plugin{} // Plugin implements the plugin.Full interface type Plugin struct { createAPISubcommand } // Name returns the name of the plugin func (Plugin) Name() string { return pluginName } // Version returns the version of the plugin func (Plugin) Version() plugin.Version { return pluginVersion } // SupportedProjectVersions returns an array with all project versions supported by the plugin func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions } // GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand } // PluginConfig defines the structure that will be used to track the data type PluginConfig struct { Resources []ResourceData `json:"resources,omitempty"` } // ResourceData store the resource data used in the plugin type ResourceData struct { Group string `json:"group,omitempty"` Domain string `json:"domain,omitempty"` Version string `json:"version"` Kind string `json:"kind"` Options options `json:"options,omitempty"` } type options struct { Image string `json:"image,omitempty"` ContainerCommand string `json:"containerCommand,omitempty"` ContainerPort string `json:"containerPort,omitempty"` RunAsUser string `json:"runAsUser,omitempty"` } // Description returns a short description of the plugin func (Plugin) Description() string { return "Scaffolds a CRD+controller to deploy an image-based Operand" } // DeprecationWarning define the deprecation message or return empty when plugin is not deprecated func (p Plugin) DeprecationWarning() string { return "" } ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/plugin_test.go ================================================ /* Copyright 2025 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 v1alpha1 import ( "testing" "sigs.k8s.io/kubebuilder/v4/pkg/plugin" ) // TestGetPluginKeyForConfigIntegration tests that the plugin correctly resolves // its key based on the plugin chain, supporting custom bundle names. func TestGetPluginKeyForConfigIntegration(t *testing.T) { p := Plugin{} tests := []struct { name string pluginChain []string expected string description string }{ { name: "exact match", pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.go.kubebuilder.io/v1-alpha"}, expected: "deploy-image.go.kubebuilder.io/v1-alpha", description: "When plugin is used directly, it should use its own key", }, { name: "bundle match with custom domain", pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.custom-domain/v1-alpha"}, expected: "deploy-image.custom-domain/v1-alpha", description: "When plugin is wrapped in bundle with custom domain, it should use bundle's key", }, { name: "bundle match with operator-sdk domain", pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.operator-sdk.io/v1-alpha"}, expected: "deploy-image.operator-sdk.io/v1-alpha", description: "When plugin is wrapped in operator-sdk bundle, it should use bundle's key", }, { name: "no match - fallback to plugin key", pluginChain: []string{"go.kubebuilder.io/v4"}, expected: "deploy-image.go.kubebuilder.io/v1-alpha", description: "When no matching key in chain, fallback to plugin's own key", }, { name: "version mismatch - fallback", pluginChain: []string{"go.kubebuilder.io/v4", "deploy-image.custom-domain/v2-alpha"}, expected: "deploy-image.go.kubebuilder.io/v1-alpha", description: "When version doesn't match, fallback to plugin's own key", }, { name: "base name mismatch - fallback", pluginChain: []string{"go.kubebuilder.io/v4", "other-plugin.custom-domain/v1-alpha"}, expected: "deploy-image.go.kubebuilder.io/v1-alpha", description: "When base name doesn't match, fallback to plugin's own key", }, { name: "multiple bundles - choose first match", pluginChain: []string{"deploy-image.bundle-a.example.com/v1-alpha", "deploy-image.bundle-b.example.com/v1-alpha"}, expected: "deploy-image.bundle-a.example.com/v1-alpha", description: "When multiple bundle keys match, select the first occurrence", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := plugin.GetPluginKeyForConfig(tt.pluginChain, p) if result != tt.expected { t.Errorf("%s: expected key %q, got %q", tt.description, tt.expected, result) } }) } } ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/api.go ================================================ /* Copyright 2022 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 scaffolds import ( "errors" "fmt" log "log/slog" "path/filepath" "strings" "github.com/spf13/afero" "sigs.k8s.io/kubebuilder/v4/pkg/config" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util" "sigs.k8s.io/kubebuilder/v4/pkg/plugins" kustomizev2scaffolds "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples" "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers" golangv4scaffolds "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds" ) var _ plugins.Scaffolder = &apiScaffolder{} // apiScaffolder contains configuration for generating scaffolding for Go type // representing the API and controller that implements the behavior for the API. type apiScaffolder struct { config config.Config resource resource.Resource image string command string port string runAsUser string // fs is the filesystem that will be used by the scaffolder fs machinery.Filesystem } // NewDeployImageScaffolder returns a new Scaffolder for declarative func NewDeployImageScaffolder(cfg config.Config, res resource.Resource, image, command, port, runAsUser string, ) plugins.Scaffolder { return &apiScaffolder{ config: cfg, resource: res, image: image, command: command, port: port, runAsUser: runAsUser, } } // InjectFS implements cmdutil.Scaffolder func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) { s.fs = fs } // Scaffold implements cmdutil.Scaffolder func (s *apiScaffolder) Scaffold() error { log.Info("Writing scaffold for you to edit...") if err := s.scaffoldCreateAPI(); err != nil { return err } // Define the boilerplate file path boilerplatePath := filepath.Join("hack", "boilerplate.go.txt") // Load the boilerplate boilerplate, err := afero.ReadFile(s.fs.FS, boilerplatePath) if err != nil { if errors.Is(err, afero.ErrFileNotFound) { log.Warn("unable to find boilerplate file. "+ "This file is used to generate the license header in the project.\n"+ "Note that controller-gen will also use this. Ensure that you "+ "add the license file or configure your project accordingly", "file_path", boilerplatePath, "error", err) boilerplate = []byte("") } else { return fmt.Errorf("error scaffolding API/controller: failed to load boilerplate: %w", err) } } // Initialize the machinery.Scaffold that will write the files to disk scaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config), machinery.WithBoilerplate(string(boilerplate)), machinery.WithResource(&s.resource), ) if err := scaffold.Execute( &api.Types{Port: s.port}, ); err != nil { return fmt.Errorf("error updating APIs: %w", err) } if err := scaffold.Execute( &samples.CRDSample{Port: s.port}, ); err != nil { return fmt.Errorf("error updating config/samples: %w", err) } controller := &controllers.Controller{ ControllerRuntimeVersion: golangv4scaffolds.ControllerRuntimeVersion, } if err := scaffold.Execute( controller, ); err != nil { return fmt.Errorf("error scaffolding controller: %w", err) } if err := s.updateControllerCode(*controller); err != nil { return fmt.Errorf("error updating controller: %w", err) } defaultMainPath := "cmd/main.go" if err := s.updateMainByAddingEventRecorder(defaultMainPath); err != nil { return fmt.Errorf("error updating main.go: %w", err) } if err := scaffold.Execute( &controllers.ControllerTest{Port: s.port}, ); err != nil { return fmt.Errorf("error creating controller/**_controller_test.go: %w", err) } return s.addEnvVarIntoManager() } // addEnvVarIntoManager will update the config/manager/manager.yaml by adding // a new ENV VAR for to store the image informed which will be used in the // controller to create the Pod for the Kind func (s *apiScaffolder) addEnvVarIntoManager() error { managerPath := filepath.Join("config", "manager", "manager.yaml") err := util.ReplaceInFile(managerPath, `env:`, `env:`) if err != nil { if err = util.InsertCode(managerPath, `name: manager`, "\n env:"); err != nil { return fmt.Errorf("error scaffolding env key in config/manager/manager.yaml") } } if err = util.InsertCode(managerPath, `env:`, fmt.Sprintf(envVarTemplate, strings.ToUpper(s.resource.Kind), s.image)); err != nil { return fmt.Errorf("error scaffolding env key in config/manager/manager.yaml") } return nil } // scaffoldCreateAPI will reuse the code from the kustomize and base golang // plugins to do the default scaffolds which an API is created func (s *apiScaffolder) scaffoldCreateAPI() error { if err := s.scaffoldCreateAPIFromGolang(); err != nil { return fmt.Errorf("error scaffolding golang files for the new API: %w", err) } if err := s.scaffoldCreateAPIFromKustomize(); err != nil { return fmt.Errorf("error scaffolding kustomize manifests for the new API: %w", err) } return nil } // TODO: replace this implementation by creating its own MainUpdater // which will have its own controller template which set the recorder so that we can use it // in the reconciliation to create an event inside for the finalizer func (s *apiScaffolder) updateMainByAddingEventRecorder(defaultMainPath string) error { if err := util.InsertCode( defaultMainPath, fmt.Sprintf( `%sReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(),`, s.resource.Kind), fmt.Sprintf(recorderTemplate, strings.ToLower(s.resource.Kind)), ); err != nil { return fmt.Errorf("error scaffolding event recorder in %q: %w", defaultMainPath, err) } return nil } // updateControllerCode will update the code generate on the template to add the Container information func (s *apiScaffolder) updateControllerCode(controller controllers.Controller) error { if err := util.ReplaceInFile( controller.Path, "//TODO: scaffold container", fmt.Sprintf(containerTemplate, // value for the image strings.ToLower(s.resource.Kind), // value for the name of the container ), ); err != nil { return fmt.Errorf("error scaffolding container in the controller path %q: %w", controller.Path, err) } // Scaffold the command if informed if len(s.command) > 0 { // TODO: improve it to be an spec in the sample and api instead so that // users can change the values var res string for value := range strings.SplitSeq(s.command, ",") { res += fmt.Sprintf(" \"%s\",", strings.TrimSpace(value)) } // remove the latest , res = res[:len(res)-1] // remove the first space to not fail in the go fmt ./... res = strings.TrimLeft(res, " ") if err := util.InsertCode(controller.Path, `SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), AllowPrivilegeEscalation: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", }, }, },`, fmt.Sprintf(commandTemplate, res)); err != nil { return fmt.Errorf("error scaffolding command in the controller path %q: %w", controller.Path, err) } } // Scaffold the port if informed if len(s.port) > 0 { if err := util.InsertCode( controller.Path, `SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), AllowPrivilegeEscalation: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", }, }, },`, fmt.Sprintf( portTemplate, strings.ToLower(s.resource.Kind), strings.ToLower(s.resource.Kind)), ); err != nil { return fmt.Errorf("error scaffolding container port in the controller path %q: %w", controller.Path, err) } } if len(s.runAsUser) > 0 { if err := util.InsertCode( controller.Path, `RunAsNonRoot: ptr.To(true),`, fmt.Sprintf(runAsUserTemplate, s.runAsUser), ); err != nil { return fmt.Errorf("error scaffolding user-id in the controller path %q: %w", controller.Path, err) } } return nil } func (s *apiScaffolder) scaffoldCreateAPIFromKustomize() error { kustomizeScaffolder := kustomizev2scaffolds.NewAPIScaffolder( s.config, s.resource, true, ) kustomizeScaffolder.InjectFS(s.fs) if err := kustomizeScaffolder.Scaffold(); err != nil { return fmt.Errorf("error scaffolding kustomize files for the APIs: %w", err) } return nil } func (s *apiScaffolder) scaffoldCreateAPIFromGolang() error { golangV4Scaffolder := golangv4scaffolds.NewAPIScaffolder(s.config, s.resource, true) golangV4Scaffolder.InjectFS(s.fs) if err := golangV4Scaffolder.Scaffold(); err != nil { return fmt.Errorf("error scaffolding golang files for the APIs: %v", err) } return nil } const containerTemplate = `Containers: []corev1.Container{{ Image: image, Name: "%s", ImagePullPolicy: corev1.PullIfNotPresent, // Ensure restrictive context for the container // More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted SecurityContext: &corev1.SecurityContext{ RunAsNonRoot: ptr.To(true), AllowPrivilegeEscalation: ptr.To(false), Capabilities: &corev1.Capabilities{ Drop: []corev1.Capability{ "ALL", }, }, }, }}` const runAsUserTemplate = ` RunAsUser: ptr.To(int64(%s)),` const commandTemplate = ` Command: []string{%s},` const portTemplate = ` Ports: []corev1.ContainerPort{{ ContainerPort: %s.Spec.ContainerPort, Name: "%s", }},` const recorderTemplate = ` Recorder: mgr.GetEventRecorder("%s-controller"),` const envVarTemplate = ` - name: %s_IMAGE value: %s` ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/api/types.go ================================================ /* Copyright 2022 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 api import ( log "log/slog" "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Types{} // Types scaffolds the file that defines the schema for a CRD // type Types struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.BoilerplateMixin machinery.ResourceMixin // Port if informed we will create the scaffold with this spec Port string } // SetTemplateDefaults implements machinery.Template func (f *Types) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_types.go") } else { f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go") } } f.Path = f.Resource.Replacer().Replace(f.Path) log.Info(f.Path) f.TemplateBody = typesTemplate f.IfExistsAction = machinery.OverwriteFile return nil } //nolint:lll const typesTemplate = `{{ .Boilerplate }} package {{ .Resource.Version }} import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. // {{ .Resource.Kind }}Spec defines the desired state of {{ .Resource.Kind }} type {{ .Resource.Kind }}Spec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file // The following markers will use OpenAPI v3 schema to validate the value // More info: https://book.kubebuilder.io/reference/markers/crd-validation.html // size defines the number of {{ .Resource.Kind }} instances // +kubebuilder:default=1 // +kubebuilder:validation:Minimum=0 // +optional Size *int32 ` + "`" + `json:"size,omitempty"` + "`" + ` {{ if not (isEmptyStr .Port) -}} // containerPort defines the port that will be used to init the container with the image // +required ContainerPort int32 ` + "`" + `json:"containerPort"` + "`" + ` {{- end }} } // {{ .Resource.Kind }}Status defines the observed state of {{ .Resource.Kind }} type {{ .Resource.Kind }}Status struct { // For Kubernetes API conventions, see: // https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties // conditions represent the current state of the {{ .Resource.Kind }} resource. // Each condition has a unique type and reflects the status of a specific aspect of the resource. // // Standard condition types include: // - "Available": the resource is fully functional // - "Progressing": the resource is being created or updated // - "Degraded": the resource failed to reach or maintain its desired state // // The status of each condition is one of True, False, or Unknown. // +listType=map // +listMapKey=type // +optional Conditions []metav1.Condition ` + "`" + `json:"conditions,omitempty"` + "`" + ` } // +kubebuilder:object:root=true // +kubebuilder:subresource:status {{- if and (not .Resource.API.Namespaced) (not .Resource.IsRegularPlural) }} // +kubebuilder:resource:path={{ .Resource.Plural }},scope=Cluster {{- else if not .Resource.API.Namespaced }} // +kubebuilder:resource:scope=Cluster {{- else if not .Resource.IsRegularPlural }} // +kubebuilder:resource:path={{ .Resource.Plural }} {{- end }} // {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API type {{ .Resource.Kind }} struct { metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` // metadata is a standard object metadata // +optional metav1.ObjectMeta ` + "`" + `json:"metadata,omitzero"` + "`" + ` // spec defines the desired state of {{ .Resource.Kind }} // +required Spec {{ .Resource.Kind }}Spec ` + "`" + `json:"spec"` + "`" + ` // status defines the observed state of {{ .Resource.Kind }} // +optional Status {{ .Resource.Kind }}Status ` + "`" + `json:"status,omitzero"` + "`" + ` } // +kubebuilder:object:root=true // {{ .Resource.Kind }}List contains a list of {{ .Resource.Kind }} type {{ .Resource.Kind }}List struct { metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + ` metav1.ListMeta ` + "`" + `json:"metadata,omitzero"` + "`" + ` Items []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + ` } func init() { SchemeBuilder.Register(&{{ .Resource.Kind }}{}, &{{ .Resource.Kind }}List{}) } ` ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/config/samples/crd_sample.go ================================================ /* Copyright 2022 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 samples import ( log "log/slog" "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &CRDSample{} // CRDSample scaffolds a file that defines a sample manifest for the CRD type CRDSample struct { machinery.TemplateMixin machinery.ResourceMixin machinery.ProjectNameMixin // Port if informed we will create the scaffold with this spec Port string } // SetTemplateDefaults implements machinery.Template func (f *CRDSample) SetTemplateDefaults() error { if f.Path == "" { if f.Resource.Group != "" { f.Path = filepath.Join("config", "samples", "%[group]_%[version]_%[kind].yaml") } else { f.Path = filepath.Join("config", "samples", "%[version]_%[kind].yaml") } } f.Path = f.Resource.Replacer().Replace(f.Path) log.Info(f.Path) f.IfExistsAction = machinery.OverwriteFile f.TemplateBody = crdSampleTemplate return nil } const crdSampleTemplate = `apiVersion: {{ .Resource.QualifiedGroup }}/{{ .Resource.Version }} kind: {{ .Resource.Kind }} metadata: labels: app.kubernetes.io/name: {{ .ProjectName }} app.kubernetes.io/managed-by: kustomize name: {{ lower .Resource.Kind }}-sample spec: # TODO(user): edit the following value to ensure the number # of Pods/Instances your Operand must have on cluster size: 1 {{ if not (isEmptyStr .Port) }} # TODO(user): edit the following value to ensure the container has the right port to be initialized containerPort: {{ .Port }} {{ end -}} ` ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers/controller-test.go ================================================ /* Copyright 2022 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 controllers import ( log "log/slog" "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &ControllerTest{} // ControllerTest scaffolds the file that defines tests for the controller for a CRD or a builtin resource // type ControllerTest struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.BoilerplateMixin machinery.ResourceMixin Port string PackageName string } // SetTemplateDefaults implements machinery.Template func (f *ControllerTest) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("internal", "controller", "%[group]", "%[kind]_controller_test.go") } else { f.Path = filepath.Join("internal", "controller", "%[kind]_controller_test.go") } } f.Path = f.Resource.Replacer().Replace(f.Path) log.Info(f.Path) f.PackageName = "controller" f.IfExistsAction = machinery.OverwriteFile log.Info("creating import for resource", "resource", f.Resource.Path) f.TemplateBody = controllerTestTemplate return nil } const controllerTestTemplate = `{{ .Boilerplate }} package {{ if and .MultiGroup .Resource.Group }}{{ .Resource.PackageName }}{{ else }}{{ .PackageName }}{{ end }} import ( "context" "os" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/reconcile" {{ if not (isEmptyStr .Resource.Path) -}} {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" {{- end }} ) var _ = Describe("{{ .Resource.Kind }} controller", func() { Context("{{ .Resource.Kind }} controller test", func() { const {{ .Resource.Kind }}Name = "test-{{ lower .Resource.Kind }}" ctx := context.Background() namespace := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: {{ .Resource.Kind }}Name, Namespace: {{ .Resource.Kind }}Name, }, } typeNamespacedName := types.NamespacedName{ Name: {{ .Resource.Kind }}Name, Namespace: {{ .Resource.Kind }}Name, } {{ lower .Resource.Kind }} := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} SetDefaultEventuallyTimeout(2 * time.Minute) SetDefaultEventuallyPollingInterval(time.Second) BeforeEach(func() { By("Creating the Namespace to perform the tests") err := k8sClient.Create(ctx, namespace); Expect(err).NotTo(HaveOccurred()) By("Setting the Image ENV VAR which stores the Operand image") err= os.Setenv("{{ upper .Resource.Kind }}_IMAGE", "example.com/image:test") Expect(err).NotTo(HaveOccurred()) By("creating the custom resource for the Kind {{ .Resource.Kind }}") err = k8sClient.Get(ctx, typeNamespacedName, {{ lower .Resource.Kind }}) if err != nil && errors.IsNotFound(err) { // Let's mock our custom resource at the same way that we would // apply on the cluster the manifest under config/samples {{ lower .Resource.Kind }} = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{ ObjectMeta: metav1.ObjectMeta{ Name: {{ .Resource.Kind }}Name, Namespace: namespace.Name, }, Spec: {{ .Resource.ImportAlias }}.{{ .Resource.Kind }}Spec{ Size: ptr.To(int32(1)), {{ if not (isEmptyStr .Port) -}} ContainerPort: {{ .Port }}, {{- end }} }, } err = k8sClient.Create(ctx, {{ lower .Resource.Kind }}) Expect(err).NotTo(HaveOccurred()) } }) AfterEach(func() { By("removing the custom resource for the Kind {{ .Resource.Kind }}") found := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} err := k8sClient.Get(ctx, typeNamespacedName, found) Expect(err).NotTo(HaveOccurred()) Eventually(func(g Gomega) { g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed()) }).Should(Succeed()) // TODO(user): Attention if you improve this code by adding other context test you MUST // be aware of the current delete namespace limitations. // More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations By("Deleting the Namespace to perform the tests") _ = k8sClient.Delete(ctx, namespace); By("Removing the Image ENV VAR which stores the Operand image") _ = os.Unsetenv("{{ upper .Resource.Kind }}_IMAGE") }) It("should successfully reconcile a custom resource for {{ .Resource.Kind }}", func() { By("Checking if the custom resource was successfully created") Eventually(func(g Gomega) { found := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed()) }).Should(Succeed()) By("Reconciling the custom resource created") {{ lower .Resource.Kind }}Reconciler := &{{ .Resource.Kind }}Reconciler{ Client: k8sClient, Scheme: k8sClient.Scheme(), } _, err := {{ lower .Resource.Kind }}Reconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) By("Checking if Deployment was successfully created in the reconciliation") Eventually(func(g Gomega) { found := &appsv1.Deployment{} g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed()) }).Should(Succeed()) By("Reconciling the custom resource again") _, err = {{ lower .Resource.Kind }}Reconciler.Reconcile(ctx, reconcile.Request{ NamespacedName: typeNamespacedName, }) Expect(err).NotTo(HaveOccurred()) By("Checking the latest Status Condition added to the {{ .Resource.Kind }} instance") Expect(k8sClient.Get(ctx, typeNamespacedName, {{ lower .Resource.Kind }})).To(Succeed()) var conditions []metav1.Condition Expect({{ lower .Resource.Kind }}.Status.Conditions).To(ContainElement( HaveField("Type", Equal(typeAvailable{{ .Resource.Kind }})), &conditions)) Expect(conditions).To(HaveLen(1), "Multiple conditions of type %s", typeAvailable{{ .Resource.Kind }}) Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "condition %s", typeAvailable{{ .Resource.Kind }}) Expect(conditions[0].Reason).To(Equal("Reconciling"), "condition %s", typeAvailable{{ .Resource.Kind }}) }) }) }) ` ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/scaffolds/internal/templates/controllers/controller.go ================================================ /* Copyright 2022 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 controllers import ( log "log/slog" "path/filepath" "sigs.k8s.io/kubebuilder/v4/pkg/machinery" ) var _ machinery.Template = &Controller{} // Controller scaffolds the file that defines the controller for a CRD or a builtin resource // type Controller struct { machinery.TemplateMixin machinery.MultiGroupMixin machinery.BoilerplateMixin machinery.ResourceMixin machinery.ProjectNameMixin machinery.NamespacedMixin ControllerRuntimeVersion string PackageName string } // SetTemplateDefaults implements machinery.Template func (f *Controller) SetTemplateDefaults() error { if f.Path == "" { if f.MultiGroup && f.Resource.Group != "" { f.Path = filepath.Join("internal", "controller", "%[group]", "%[kind]_controller.go") } else { f.Path = filepath.Join("internal", "controller", "%[kind]_controller.go") } } f.Path = f.Resource.Replacer().Replace(f.Path) log.Info(f.Path) f.PackageName = "controller" log.Info("creating import for resource", "resource_path", f.Resource.Path) f.TemplateBody = controllerTemplate // This one is to overwrite the controller if it exist f.IfExistsAction = machinery.OverwriteFile return nil } //nolint:lll const controllerTemplate = `{{ .Boilerplate }} package {{ if and .MultiGroup .Resource.Group }}{{ .Resource.PackageName }}{{ else }}{{ .PackageName }}{{ end }} import ( "context" "strings" "time" "fmt" "os" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/tools/events" "k8s.io/utils/ptr" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" {{ if not (isEmptyStr .Resource.Path) -}} {{ .Resource.ImportAlias }} "{{ .Resource.Path }}" {{- end }} ) const {{ lower .Resource.Kind }}Finalizer = "{{ .Resource.Group }}.{{ .Resource.Domain }}/finalizer" // Definitions to manage status conditions const ( // typeAvailable{{ .Resource.Kind }} represents the status of the Deployment reconciliation typeAvailable{{ .Resource.Kind }} = "Available" // typeDegraded{{ .Resource.Kind }} represents the status used when the custom resource is deleted and the finalizer operations are yet to occur. typeDegraded{{ .Resource.Kind }} = "Degraded" ) // {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object type {{ .Resource.Kind }}Reconciler struct { client.Client Scheme *runtime.Scheme Recorder events.EventRecorder } // The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen // when the command is executed. // To know more about markers see: https://book.kubebuilder.io/reference/markers.html {{ if .Namespaced -}} // +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},namespace={{ .ProjectName }}-system,resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},namespace={{ .ProjectName }}-system,resources={{ .Resource.Plural }}/status,verbs=get;update;patch // +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},namespace={{ .ProjectName }}-system,resources={{ .Resource.Plural }}/finalizers,verbs=update // +kubebuilder:rbac:groups=events.k8s.io,namespace={{ .ProjectName }}-system,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,namespace={{ .ProjectName }}-system,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,namespace={{ .ProjectName }}-system,resources=pods,verbs=get;list;watch {{- else -}} // +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch // +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/finalizers,verbs=update // +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch {{- end }} // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // It is essential for the controller's reconciliation loop to be idempotent. By following the Operator // pattern you will create Controllers which provide a reconcile function // responsible for synchronizing resources until the desired state is reached on the cluster. // Breaking this recommendation goes against the design principles of controller-runtime. // and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention. // For further info: // - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/ // - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/ // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/reconcile func (r *{{ .Resource.Kind }}Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logf.FromContext(ctx) // Fetch the {{ .Resource.Kind }} instance // The purpose is check if the Custom Resource for the Kind {{ .Resource.Kind }} // is applied on the cluster if not we return nil to stop the reconciliation {{ lower .Resource.Kind }} := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{} err := r.Get(ctx, req.NamespacedName, {{ lower .Resource.Kind }}) if err != nil { if apierrors.IsNotFound(err) { // If the custom resource is not found then it usually means that it was deleted or not created // In this way, we will stop the reconciliation log.Info("{{ .Resource.Kind }} resource not found, ignoring since object must be deleted") return ctrl.Result{}, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get {{ lower .Resource.Kind }}") return ctrl.Result{}, err } if len({{ lower .Resource.Kind }}.Status.Conditions) == 0 { meta.SetStatusCondition(&{{ lower .Resource.Kind }}.Status.Conditions, metav1.Condition{Type: typeAvailable{{ .Resource.Kind }}, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"}) if err = r.Status().Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to update {{ .Resource.Kind }} status") return ctrl.Result{}, err } // Let's re-fetch the {{ lower .Resource.Kind }} Custom Resource after updating the status // so that we have the latest state of the resource on the cluster and we will avoid // raising the error "the object has been modified, please apply // your changes to the latest version and try again" which would re-trigger the reconciliation // if we try to update it again in the following operations if err := r.Get(ctx, req.NamespacedName, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to re-fetch {{ lower .Resource.Kind }}") return ctrl.Result{}, err } } // Let's add a finalizer. Then, we can define some operations which should // occur before the custom resource is deleted. // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers if !controllerutil.ContainsFinalizer({{ lower .Resource.Kind }}, {{ lower .Resource.Kind }}Finalizer) { log.Info("Adding finalizer for {{ .Resource.Kind }}") controllerutil.AddFinalizer({{ lower .Resource.Kind }}, {{ lower .Resource.Kind }}Finalizer) if err = r.Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to update custom resource to add finalizer") return ctrl.Result{}, err } } // Check if the {{ .Resource.Kind }} instance is marked to be deleted, which is // indicated by the deletion timestamp being set. is{{ .Resource.Kind }}MarkedToBeDeleted := {{ lower .Resource.Kind }}.GetDeletionTimestamp() != nil if is{{ .Resource.Kind }}MarkedToBeDeleted { if controllerutil.ContainsFinalizer({{ lower .Resource.Kind }}, {{ lower .Resource.Kind }}Finalizer) { log.Info("Performing finalizer operations for {{ .Resource.Kind }} before deleting CR") // Let's add here a status "Downgrade" to reflect that this resource began its process to be terminated. meta.SetStatusCondition(&{{ lower .Resource.Kind }}.Status.Conditions, metav1.Condition{Type: typeDegraded{{ .Resource.Kind }}, Status: metav1.ConditionUnknown, Reason: "Finalizing", Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", {{ lower .Resource.Kind }}.Name)}) if err := r.Status().Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to update {{ .Resource.Kind }} status") return ctrl.Result{}, err } // Perform all operations required before removing the finalizer and allow // the Kubernetes API to remove the custom resource. r.doFinalizerOperationsFor{{ .Resource.Kind }}({{ lower .Resource.Kind }}) // TODO(user): If you add operations to the doFinalizerOperationsFor{{ .Resource.Kind }} method // then you need to ensure that all worked fine before deleting and updating the Downgrade status // otherwise, you should requeue here. // Re-fetch the {{ lower .Resource.Kind }} Custom Resource before updating the status // so that we have the latest state of the resource on the cluster and we will avoid // raising the error "the object has been modified, please apply // your changes to the latest version and try again" which would re-trigger the reconciliation if err := r.Get(ctx, req.NamespacedName, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to re-fetch {{ lower .Resource.Kind }}") return ctrl.Result{}, err } meta.SetStatusCondition(&{{ lower .Resource.Kind }}.Status.Conditions, metav1.Condition{Type: typeDegraded{{ .Resource.Kind }}, Status: metav1.ConditionTrue, Reason: "Finalizing", Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", {{ lower .Resource.Kind }}.Name)}) if err := r.Status().Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to update {{ .Resource.Kind }} status") return ctrl.Result{}, err } log.Info("Removing finalizer for {{ .Resource.Kind }} after successfully performing the operations") if ok:= controllerutil.RemoveFinalizer({{ lower .Resource.Kind }}, {{ lower .Resource.Kind }}Finalizer); !ok{ err = fmt.Errorf("finalizer for {{ .Resource.Kind }} was not removed") log.Error(err, "Failed to remove finalizer for {{ .Resource.Kind }}") return ctrl.Result{}, err } if err := r.Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to remove finalizer for {{ .Resource.Kind }}") return ctrl.Result{}, err } } return ctrl.Result{}, nil } // Check if the deployment already exists, if not create a new one found := &appsv1.Deployment{} err = r.Get(ctx, types.NamespacedName{Name: {{ lower .Resource.Kind }}.Name, Namespace: {{ lower .Resource.Kind }}.Namespace}, found) if err != nil && apierrors.IsNotFound(err) { // Define a new deployment dep, err := r.deploymentFor{{ .Resource.Kind }}({{ lower .Resource.Kind }}) if err != nil { log.Error(err, "Failed to define new Deployment resource for {{ .Resource.Kind }}") // The following implementation will update the status meta.SetStatusCondition(&{{ lower .Resource.Kind }}.Status.Conditions, metav1.Condition{Type: typeAvailable{{ .Resource.Kind }}, Status: metav1.ConditionFalse, Reason: "Reconciling", Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", {{ lower .Resource.Kind }}.Name, err)}) if err := r.Status().Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to update {{ .Resource.Kind }} status") return ctrl.Result{}, err } return ctrl.Result{}, err } log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) if err = r.Create(ctx, dep); err != nil { log.Error(err, "Failed to create new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name) return ctrl.Result{}, err } // Deployment created successfully // We will requeue the reconciliation so that we can ensure the state // and move forward for the next operations return ctrl.Result{RequeueAfter: time.Minute}, nil } else if err != nil { log.Error(err, "Failed to get Deployment") // Let's return the error for the reconciliation be re-triggered again return ctrl.Result{}, err } // If the size is not defined in the Custom Resource then we will set the desired replicas to 0 var desiredReplicas int32 = 0 if {{ lower .Resource.Kind }}.Spec.Size != nil { desiredReplicas = *{{ lower .Resource.Kind }}.Spec.Size } // The CRD API defines that the {{ .Resource.Kind }} type have a {{ .Resource.Kind }}Spec.Size field // to set the quantity of Deployment instances to the desired state on the cluster. // Therefore, the following code will ensure the Deployment size is the same as defined // via the Size spec of the Custom Resource which we are reconciling. if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas { found.Spec.Replicas = ptr.To(desiredReplicas) if err = r.Update(ctx, found); err != nil { log.Error(err, "Failed to update Deployment", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name) // Re-fetch the {{ lower .Resource.Kind }} Custom Resource before updating the status // so that we have the latest state of the resource on the cluster and we will avoid // raising the error "the object has been modified, please apply // your changes to the latest version and try again" which would re-trigger the reconciliation if err := r.Get(ctx, req.NamespacedName, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to re-fetch {{ lower .Resource.Kind }}") return ctrl.Result{}, err } // The following implementation will update the status meta.SetStatusCondition(&{{ lower .Resource.Kind }}.Status.Conditions, metav1.Condition{Type: typeAvailable{{ .Resource.Kind }}, Status: metav1.ConditionFalse, Reason: "Resizing", Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", {{ lower .Resource.Kind }}.Name, err)}) if err := r.Status().Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to update {{ .Resource.Kind }} status") return ctrl.Result{}, err } return ctrl.Result{}, err } // Now, that we update the size we want to requeue the reconciliation // so that we can ensure that we have the latest state of the resource before // update. Also, it will help ensure the desired state on the cluster return ctrl.Result{Requeue: true}, nil } // The following implementation will update the status meta.SetStatusCondition(&{{ lower .Resource.Kind }}.Status.Conditions, metav1.Condition{Type: typeAvailable{{ .Resource.Kind }}, Status: metav1.ConditionTrue, Reason: "Reconciling", Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", {{ lower .Resource.Kind }}.Name, desiredReplicas)}) if err := r.Status().Update(ctx, {{ lower .Resource.Kind }}); err != nil { log.Error(err, "Failed to update {{ .Resource.Kind }} status") return ctrl.Result{}, err } return ctrl.Result{}, nil } // finalize{{ .Resource.Kind }} will perform the required operations before delete the CR. func (r *{{ .Resource.Kind }}Reconciler) doFinalizerOperationsFor{{ .Resource.Kind }}(cr *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) { // TODO(user): Add the cleanup steps that the operator // needs to do before the CR can be deleted. Examples // of finalizers include performing backups and deleting // resources that are not owned by this CR, like a PVC. // Note: It is not recommended to use finalizers with the purpose of deleting resources which are // created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile, // are defined as dependent of the custom resource. See that we use the method ctrl.SetControllerReference. // to set the ownerRef which means that the Deployment will be deleted by the Kubernetes API. // More info: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/ // The following implementation will raise an event r.Recorder.Eventf(cr, nil, corev1.EventTypeWarning, "Deleting", "DeleteCR", "Custom Resource %s is being deleted from the namespace %s", cr.Name, cr.Namespace) } // deploymentFor{{ .Resource.Kind }} returns a {{ .Resource.Kind }} Deployment object func (r *{{ .Resource.Kind }}Reconciler) deploymentFor{{ .Resource.Kind }}( {{ lower .Resource.Kind }} *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) (*appsv1.Deployment, error) { ls := labelsFor{{ .Resource.Kind }}() // Get the Operand image image, err := imageFor{{ .Resource.Kind }}() if err != nil { return nil, err } dep := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: {{ lower .Resource.Kind }}.Name, Namespace: {{ lower .Resource.Kind }}.Namespace, }, Spec: appsv1.DeploymentSpec{ Replicas: {{ lower .Resource.Kind }}.Spec.Size, Selector: &metav1.LabelSelector{ MatchLabels: ls, }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: ls, }, Spec: corev1.PodSpec{ // TODO(user): Uncomment the following code to configure the nodeAffinity expression // according to the platforms which are supported by your solution. It is considered // best practice to support multiple architectures. build your manager image using the // makefile target docker-buildx. Also, you can use docker manifest inspect // to check what are the platforms supported. // More info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity // Affinity: &corev1.Affinity{ // NodeAffinity: &corev1.NodeAffinity{ // RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{ // NodeSelectorTerms: []corev1.NodeSelectorTerm{ // { // MatchExpressions: []corev1.NodeSelectorRequirement{ // { // Key: "kubernetes.io/arch", // Operator: "In", // Values: []string{"amd64", "arm64", "ppc64le", "s390x"}, // }, // { // Key: "kubernetes.io/os", // Operator: "In", // Values: []string{"linux"}, // }, // }, // }, // }, // }, // }, // }, SecurityContext: &corev1.PodSecurityContext{ RunAsNonRoot: ptr.To(true), // IMPORTANT: seccomProfile was introduced with Kubernetes 1.19 // If you are looking for to produce solutions to be supported // on lower versions you must remove this option. SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, //TODO: scaffold container, }, }, }, } // Set the ownerRef for the Deployment // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/ if err := ctrl.SetControllerReference({{ lower .Resource.Kind }}, dep, r.Scheme); err != nil { return nil, err } return dep, nil } // labelsFor{{ .Resource.Kind }} returns the labels for selecting the resources // More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/ func labelsFor{{ .Resource.Kind }}() map[string]string { var imageTag string image, err := imageFor{{ .Resource.Kind }}() if err == nil { imageTag = strings.Split(image, ":")[1] } return map[string]string{ "app.kubernetes.io/name": "{{ .ProjectName }}", "app.kubernetes.io/version": imageTag, "app.kubernetes.io/managed-by": "{{ .Resource.Kind }}Controller", } } // imageFor{{ .Resource.Kind }} gets the Operand image which is managed by this controller // from the {{ upper .Resource.Kind }}_IMAGE environment variable defined in the config/manager/manager.yaml func imageFor{{ .Resource.Kind }}() (string, error) { var imageEnvVar = "{{ upper .Resource.Kind }}_IMAGE" image, found := os.LookupEnv(imageEnvVar) if !found { return "", fmt.Errorf("unable to find %s environment variable with the image", imageEnvVar) } return image, nil } // SetupWithManager sets up the controller with the Manager. // The whole idea is to be watching the resources that matter for the controller. // When a resource that the controller is interested in changes, the Watch triggers // the controller’s reconciliation loop, ensuring that the actual state of the resource // matches the desired state as defined in the controller’s logic. // // Notice how we configured the Manager to monitor events such as the creation, update, // or deletion of a Custom Resource (CR) of the {{ .Resource.Kind }} kind, as well as any changes // to the Deployment that the controller manages and owns. func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). {{ if not (isEmptyStr .Resource.Path) -}} // Watch the {{ .Resource.Kind }} CR(s) and trigger reconciliation whenever it // is created, updated, or deleted For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}). {{- else -}} // Uncomment the following line adding a pointer to an instance of the controlled resource as an argument // For(). {{- end }} {{- if and (.MultiGroup) (not (isEmptyStr .Resource.Group)) }} Named("{{ lower .Resource.Group }}-{{ lower .Resource.Kind }}"). {{- else }} Named("{{ lower .Resource.Kind }}"). {{- end }} // Watch the Deployment managed by the {{ .Resource.Kind }}Reconciler. If any changes occur to the Deployment // owned and managed by this controller, it will trigger reconciliation, ensuring that the cluster // state aligns with the desired state. See that the ownerRef was set when the Deployment was created. Owns(&appsv1.Deployment{}). Complete(r) } ` ================================================ FILE: pkg/plugins/golang/deploy-image/v1alpha1/suite_test.go ================================================ /* Copyright 2026 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 v1alpha1 import ( "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) func TestDeployImageV1Alpha1(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, "Deploy Image V1Alpha1 Plugin Suite") } ================================================ FILE: pkg/plugins/golang/domain.go ================================================ /* Copyright 2020 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package golang import "sigs.k8s.io/kubebuilder/v4/pkg/plugins" // DefaultNameQualifier is the suffix appended to all kubebuilder plugin names for Golang operators. const DefaultNameQualifier = "go." + plugins.DefaultNameQualifier ================================================ FILE: pkg/plugins/golang/go_version.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 golang import ( "fmt" "os/exec" "regexp" "strconv" "strings" ) const ( goVerPattern = `^go(?P[0-9]+)\.(?P[0-9]+)(?:\.(?P[0-9]+)|(?P
(?:alpha|beta|rc)[0-9]+))?$`
)

var goVerRegexp = regexp.MustCompile(goVerPattern)

// GoVersion describes a Go version.
type GoVersion struct {
	major, minor, patch int
	prerelease          string
}

func (v GoVersion) String() string {
	switch {
	case v.patch != 0:
		return fmt.Sprintf("go%d.%d.%d", v.major, v.minor, v.patch)
	case v.prerelease != "":
		return fmt.Sprintf("go%d.%d%s", v.major, v.minor, v.prerelease)
	}
	return fmt.Sprintf("go%d.%d", v.major, v.minor)
}

// MustParse will panic if verStr does not match the expected Go version string spec.
func MustParse(verStr string) (v GoVersion) {
	if err := v.parse(verStr); err != nil {
		panic(err)
	}
	return v
}

func (v *GoVersion) parse(verStr string) error {
	m := goVerRegexp.FindStringSubmatch(verStr)
	if m == nil {
		return fmt.Errorf("invalid version string")
	}

	var err error

	v.major, err = strconv.Atoi(m[1])
	if err != nil {
		return fmt.Errorf("error parsing major version %q: %w", m[1], err)
	}

	v.minor, err = strconv.Atoi(m[2])
	if err != nil {
		return fmt.Errorf("error parsing minor version %q: %w", m[2], err)
	}

	if m[3] != "" {
		v.patch, err = strconv.Atoi(m[3])
		if err != nil {
			return fmt.Errorf("error parsing patch version %q: %w", m[2], err)
		}
	}

	v.prerelease = m[4]

	return nil
}

// Compare returns -1, 0, or 1 if v < other, v == other, or v > other, respectively.
func (v GoVersion) Compare(other GoVersion) int {
	if v.major > other.major {
		return 1
	}
	if v.major < other.major {
		return -1
	}

	if v.minor > other.minor {
		return 1
	}
	if v.minor < other.minor {
		return -1
	}

	if v.patch > other.patch {
		return 1
	}
	if v.patch < other.patch {
		return -1
	}

	if v.prerelease == other.prerelease {
		return 0
	}
	if v.prerelease == "" {
		return 1
	}
	if other.prerelease == "" {
		return -1
	}
	if v.prerelease > other.prerelease {
		return 1
	}
	return -1
}

// ValidateGoVersion verifies that Go is installed and the current go version is supported by a plugin.
func ValidateGoVersion(minVersion, maxVersion GoVersion) error {
	err := fetchAndCheckGoVersion(minVersion, maxVersion)
	if err != nil {
		return fmt.Errorf("you can skip this check using the --skip-go-version-check flag: %w", err)
	}
	return nil
}

func fetchAndCheckGoVersion(minVersion, maxVersion GoVersion) error {
	cmd := exec.Command("go", "version")
	out, err := cmd.Output()
	if err != nil {
		return fmt.Errorf("failed to retrieve 'go version': %v", string(out))
	}

	split := strings.Split(string(out), " ")
	if len(split) < 3 {
		return fmt.Errorf("found invalid Go version: %q", string(out))
	}
	goVer := split[2]
	if err := checkGoVersion(goVer, minVersion, maxVersion); err != nil {
		return fmt.Errorf("go version %q is incompatible: %w", goVer, err)
	}
	return nil
}

func checkGoVersion(verStr string, minVersion, maxVersion GoVersion) error {
	var version GoVersion
	if err := version.parse(verStr); err != nil {
		return err
	}

	if version.Compare(minVersion) < 0 || version.Compare(maxVersion) >= 0 {
		return fmt.Errorf("plugin requires %q <= version < %q", minVersion, maxVersion)
	}

	return nil
}


================================================
FILE: pkg/plugins/golang/go_version_test.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 golang

import (
	"errors"
	"slices"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("GoVersion", func() {
	Context("String", func() {
		It("patch is not empty", func() {
			v := GoVersion{major: 1, minor: 1, patch: 1}
			Expect(v.String()).To(Equal("go1.1.1"))
		})
		It("preRelease is not empty", func() {
			v := GoVersion{major: 1, minor: 1, prerelease: "-alpha"}
			Expect(v.String()).To(Equal("go1.1-alpha"))
		})
		It("default", func() {
			v := GoVersion{major: 1, minor: 1}
			Expect(v.String()).To(Equal("go1.1"))
		})
	})

	Context("MustParse", func() {
		It("succeeds", func() {
			v := GoVersion{major: 1, minor: 1, patch: 1}
			Expect(MustParse("go1.1.1")).To(Equal(v))
		})
		It("panics", func() {
			triggerPanic := func() {
				MustParse("go1.a")
			}
			Expect(triggerPanic).To(PanicWith(errors.New("invalid version string")))
		})
	})

	Context("parse", func() {
		var v GoVersion

		BeforeEach(func() {
			v = GoVersion{}
		})

		DescribeTable("should succeed for valid versions",
			func(version string, expected GoVersion) {
				Expect(v.parse(version)).NotTo(HaveOccurred())
				Expect(v.major).To(Equal(expected.major))
				Expect(v.minor).To(Equal(expected.minor))
				Expect(v.patch).To(Equal(expected.patch))
				Expect(v.prerelease).To(Equal(expected.prerelease))
			},
			Entry("for minor release", "go1.15", GoVersion{
				major: 1,
				minor: 15,
			}),
			Entry("for patch release", "go1.15.1", GoVersion{
				major: 1,
				minor: 15,
				patch: 1,
			}),
			Entry("for alpha release", "go1.15alpha1", GoVersion{
				major:      1,
				minor:      15,
				prerelease: "alpha1",
			}),
			Entry("for beta release", "go1.15beta1", GoVersion{
				major:      1,
				minor:      15,
				prerelease: "beta1",
			}),
			Entry("for release candidate", "go1.15rc1", GoVersion{
				major:      1,
				minor:      15,
				prerelease: "rc1",
			}),
		)

		DescribeTable("should fail for invalid versions",
			func(version string) { Expect(v.parse(version)).To(HaveOccurred()) },
			Entry("for invalid prefix", "g1.15"),
			Entry("for missing major version", "go.15"),
			Entry("for missing minor version", "go1."),
			Entry("for patch and prerelease version", "go1.15.1rc1"),
			Entry("for invalid major version", "goa.15"),
			Entry("for invalid minor version", "go1.a"),
			Entry("for invalid patch version", "go1.15.a"),
		)
	})

	Context("Compare", func() {
		// Test Compare() by sorting a list.
		var (
			versions       []GoVersion
			sortedVersions []GoVersion
		)

		BeforeEach(func() {
			versions = []GoVersion{
				{major: 1, minor: 15, prerelease: "rc2"},
				{major: 1, minor: 15, patch: 1},
				{major: 1, minor: 16},
				{major: 1, minor: 15, prerelease: "beta1"},
				{major: 1, minor: 15, prerelease: "alpha2"},
				{major: 2, minor: 0},
				{major: 1, minor: 15, prerelease: "alpha1"},
				{major: 1, minor: 13},
				{major: 1, minor: 15, prerelease: "rc1"},
				{major: 1, minor: 15},
				{major: 1, minor: 15, patch: 2},
				{major: 1, minor: 14},
				{major: 1, minor: 15, prerelease: "beta2"},
				{major: 0, minor: 123},
			}

			sortedVersions = []GoVersion{
				{major: 0, minor: 123},
				{major: 1, minor: 13},
				{major: 1, minor: 14},
				{major: 1, minor: 15, prerelease: "alpha1"},
				{major: 1, minor: 15, prerelease: "alpha2"},
				{major: 1, minor: 15, prerelease: "beta1"},
				{major: 1, minor: 15, prerelease: "beta2"},
				{major: 1, minor: 15, prerelease: "rc1"},
				{major: 1, minor: 15, prerelease: "rc2"},
				{major: 1, minor: 15},
				{major: 1, minor: 15, patch: 1},
				{major: 1, minor: 15, patch: 2},
				{major: 1, minor: 16},
				{major: 2, minor: 0},
			}
		})

		It("sorts a valid list of versions correctly", func() {
			slices.SortStableFunc(versions, func(a, b GoVersion) int {
				return a.Compare(b)
			})
			Expect(versions).To(Equal(sortedVersions))
		})
	})
})

var _ = Describe("ValidateGoVersion", func() {
	DescribeTable("should return no error for valid/supported go versions", func(minVersion, maxVersion GoVersion) {
		Expect(ValidateGoVersion(minVersion, maxVersion)).To(Succeed())
	},
		Entry("for minVersion: 1.1.1 and maxVersion: 2000.1.1", GoVersion{major: 1, minor: 1, patch: 1},
			GoVersion{major: 2000, minor: 1, patch: 1}),
		Entry("for minVersion: 1.1.1 and maxVersion: 1.2000.2000", GoVersion{major: 1, minor: 1, patch: 1},
			GoVersion{major: 1, minor: 2000, patch: 1}),
	)

	DescribeTable("should return error for invalid/unsupported go versions", func(minVersion, maxVersion GoVersion) {
		Expect(ValidateGoVersion(minVersion, maxVersion)).NotTo(Succeed())
	},
		Entry("for invalid min and maxVersions", GoVersion{major: 2, minor: 2, patch: 2},
			GoVersion{major: 1, minor: 1, patch: 1}),
	)
})

var _ = Describe("checkGoVersion", func() {
	var (
		goVerMin GoVersion
		goVerMax GoVersion
	)

	BeforeEach(func() {
		goVerMin = MustParse("go1.13")
		goVerMax = MustParse("go2.0alpha1")
	})

	DescribeTable("should return no error for supported go versions",
		func(version string) { Expect(checkGoVersion(version, goVerMin, goVerMax)).To(Succeed()) },
		Entry("for go 1.13", "go1.13"),
		Entry("for go 1.13.1", "go1.13.1"),
		Entry("for go 1.13.2", "go1.13.2"),
		Entry("for go 1.13.3", "go1.13.3"),
		Entry("for go 1.13.4", "go1.13.4"),
		Entry("for go 1.13.5", "go1.13.5"),
		Entry("for go 1.13.6", "go1.13.6"),
		Entry("for go 1.13.7", "go1.13.7"),
		Entry("for go 1.13.8", "go1.13.8"),
		Entry("for go 1.13.9", "go1.13.9"),
		Entry("for go 1.13.10", "go1.13.10"),
		Entry("for go 1.13.11", "go1.13.11"),
		Entry("for go 1.13.12", "go1.13.12"),
		Entry("for go 1.13.13", "go1.13.13"),
		Entry("for go 1.13.14", "go1.13.14"),
		Entry("for go 1.13.15", "go1.13.15"),
		Entry("for go 1.14beta1", "go1.14beta1"),
		Entry("for go 1.14rc1", "go1.14rc1"),
		Entry("for go 1.14", "go1.14"),
		Entry("for go 1.14.1", "go1.14.1"),
		Entry("for go 1.14.2", "go1.14.2"),
		Entry("for go 1.14.3", "go1.14.3"),
		Entry("for go 1.14.4", "go1.14.4"),
		Entry("for go 1.14.5", "go1.14.5"),
		Entry("for go 1.14.6", "go1.14.6"),
		Entry("for go 1.14.7", "go1.14.7"),
		Entry("for go 1.14.8", "go1.14.8"),
		Entry("for go 1.14.9", "go1.14.9"),
		Entry("for go 1.14.10", "go1.14.10"),
		Entry("for go 1.14.11", "go1.14.11"),
		Entry("for go 1.14.12", "go1.14.12"),
		Entry("for go 1.14.13", "go1.14.13"),
		Entry("for go 1.14.14", "go1.14.14"),
		Entry("for go 1.14.15", "go1.14.15"),
		Entry("for go 1.15beta1", "go1.15beta1"),
		Entry("for go 1.15rc1", "go1.15rc1"),
		Entry("for go 1.15rc2", "go1.15rc2"),
		Entry("for go 1.15", "go1.15"),
		Entry("for go 1.15.1", "go1.15.1"),
		Entry("for go 1.15.2", "go1.15.2"),
		Entry("for go 1.15.3", "go1.15.3"),
		Entry("for go 1.15.4", "go1.15.4"),
		Entry("for go 1.15.5", "go1.15.5"),
		Entry("for go 1.15.6", "go1.15.6"),
		Entry("for go 1.15.7", "go1.15.7"),
		Entry("for go 1.15.8", "go1.15.8"),
		Entry("for go 1.16", "go1.16"),
		Entry("for go 1.16.1", "go1.16.1"),
		Entry("for go 1.16.2", "go1.16.2"),
		Entry("for go 1.16.3", "go1.16.3"),
		Entry("for go 1.16.4", "go1.16.4"),
		Entry("for go 1.16.5", "go1.16.5"),
		Entry("for go 1.16.6", "go1.16.6"),
		Entry("for go 1.16.7", "go1.16.7"),
		Entry("for go 1.16.8", "go1.16.8"),
		Entry("for go 1.16.9", "go1.16.9"),
		Entry("for go 1.16.10", "go1.16.10"),
		Entry("for go 1.16.11", "go1.16.11"),
		Entry("for go 1.16.12", "go1.16.12"),
		Entry("for go 1.17.1", "go1.17.1"),
		Entry("for go 1.17.2", "go1.17.2"),
		Entry("for go 1.17.3", "go1.17.3"),
		Entry("for go 1.17.4", "go1.17.4"),
		Entry("for go 1.17.5", "go1.17.5"),
		Entry("for go 1.18.1", "go1.18.1"),
		Entry("for go.1.19", "go1.19"),
		Entry("for go.1.19.1", "go1.19.1"),
		Entry("for go.1.20", "go1.20"),
		Entry("for go.1.21", "go1.21"),
		Entry("for go.1.22", "go1.22"),
		Entry("for go.1.23", "go1.23"),
		Entry("for go.1.24", "go1.24"),
		Entry("for go.1.25", "go1.25"),
	)

	DescribeTable("should return an error for non-supported go versions",
		func(version string) { Expect(checkGoVersion(version, goVerMin, goVerMax)).NotTo(Succeed()) },
		Entry("for invalid go versions", "go"),
		Entry("for go 1.13beta1", "go1.13beta1"),
		Entry("for go 1.13rc1", "go1.13rc1"),
		Entry("for go 1.13rc2", "go1.13rc2"),
		Entry("for go 2.0alpha1", "go2.0alpha1"),
		Entry("for go 2.0.0", "go2.0.0"),
	)
})


================================================
FILE: pkg/plugins/golang/options.go
================================================
/*
Copyright 2022 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 golang

import (
	"path"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
)

var coreGroups = map[string]string{
	"admission":             "k8s.io",
	"admissionregistration": "k8s.io",
	"apps":                  "",
	"auditregistration":     "k8s.io",
	"apiextensions":         "k8s.io",
	"authentication":        "k8s.io",
	"authorization":         "k8s.io",
	"autoscaling":           "",
	"batch":                 "",
	"certificates":          "k8s.io",
	"coordination":          "k8s.io",
	"core":                  "",
	"events":                "k8s.io",
	"extensions":            "",
	"imagepolicy":           "k8s.io",
	"networking":            "k8s.io",
	"node":                  "k8s.io",
	"metrics":               "k8s.io",
	"policy":                "",
	"rbac.authorization":    "k8s.io",
	"scheduling":            "k8s.io",
	"setting":               "k8s.io",
	"storage":               "k8s.io",
}

// Options contains the information required to build a new resource.Resource.
type Options struct {
	// Plural is the resource's kind plural form.
	Plural string

	// ExternalAPIPath allows to inform a path for APIs not defined in the project
	ExternalAPIPath string

	// ExternalAPIDomain allows to inform the resource domain to build the Qualified Group
	// to generate the RBAC markers
	ExternalAPIDomain string

	// ExternalAPIModule specifies the Go module path for the external API with optional version.
	// Example: github.com/cert-manager/cert-manager@v1.18.2
	ExternalAPIModule string

	// Namespaced is true if the resource should be namespaced.
	Namespaced bool

	// Flags that define which parts should be scaffolded
	DoAPI        bool
	DoController bool
	DoDefaulting bool
	DoValidation bool
	DoConversion bool

	// Spoke versions for conversion webhook
	Spoke []string

	// DefaultingPath is the custom path for the defaulting/mutating webhook
	DefaultingPath string

	// ValidationPath is the custom path for the validation webhook
	ValidationPath string
}

// UpdateResource updates the provided resource with the options
func (opts Options) UpdateResource(res *resource.Resource, c config.Config) {
	if opts.Plural != "" {
		res.Plural = opts.Plural
	}

	if opts.DoAPI {
		res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup())

		res.API = &resource.API{
			CRDVersion: "v1",
			Namespaced: opts.Namespaced,
		}
	}

	if opts.DoController {
		res.Controller = true
	}

	if opts.DoDefaulting || opts.DoValidation || opts.DoConversion {
		res.Path = resource.APIPackagePath(c.GetRepository(), res.Group, res.Version, c.IsMultiGroup())

		res.Webhooks.WebhookVersion = "v1"
		if opts.DoDefaulting {
			res.Webhooks.Defaulting = true
			if opts.DefaultingPath != "" {
				res.Webhooks.DefaultingPath = opts.DefaultingPath
			}
		}
		if opts.DoValidation {
			res.Webhooks.Validation = true
			if opts.ValidationPath != "" {
				res.Webhooks.ValidationPath = opts.ValidationPath
			}
		}
		if opts.DoConversion {
			res.Webhooks.Conversion = true
			res.Webhooks.Spoke = opts.Spoke
		}
	}

	if len(opts.ExternalAPIPath) > 0 {
		res.External = true
		res.Path = opts.ExternalAPIPath
		if len(opts.ExternalAPIDomain) > 0 {
			res.Domain = opts.ExternalAPIDomain
		}
		// Store module path if provided
		if len(opts.ExternalAPIModule) > 0 {
			res.Module = opts.ExternalAPIModule
		}
	}

	// domain and path may need to be changed in case we are referring to a builtin core resource:
	//  - Check if we are scaffolding the resource now           => project resource
	//  - Check if we already scaffolded the resource            => project resource
	//  - Check if the resource group is a well-known core group => builtin core resource
	//  - In any other case, default to                          => project resource
	if !opts.DoAPI {
		var alreadyHasAPI bool
		loadedRes, err := c.GetResource(res.GVK)
		alreadyHasAPI = err == nil && loadedRes.HasAPI()
		if !alreadyHasAPI {
			if res.External {
				res.Path = opts.ExternalAPIPath
				res.Domain = opts.ExternalAPIDomain
			} else {
				// Handle core types
				if domain, found := coreGroups[res.Group]; found {
					res.Core = true
					res.Domain = domain
					res.Path = path.Join("k8s.io", "api", res.Group, res.Version)
				}
			}
		}
	}
}


================================================
FILE: pkg/plugins/golang/options_test.go
================================================
/*
Copyright 2022 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 golang

import (
	"path"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
)

var _ = Describe("Options", func() {
	Context("UpdateResource", func() {
		const (
			group   = "crew"
			domain  = "test.io"
			version = "v1"
			kind    = "FirstMate"
		)

		var (
			gvk resource.GVK
			cfg config.Config
		)

		BeforeEach(func() {
			gvk = resource.GVK{
				Group:   group,
				Domain:  domain,
				Version: version,
				Kind:    kind,
			}

			cfg = cfgv3.New()
			_ = cfg.SetRepository("test")
		})

		DescribeTable("should succeed",
			func(options Options) {
				for _, multiGroup := range []bool{false, true} {
					if multiGroup {
						Expect(cfg.SetMultiGroup()).To(Succeed())
					} else {
						Expect(cfg.ClearMultiGroup()).To(Succeed())
					}

					res := resource.Resource{
						GVK:      gvk,
						Plural:   "firstmates",
						API:      &resource.API{},
						Webhooks: &resource.Webhooks{},
					}

					options.UpdateResource(&res, cfg)
					Expect(res.Validate()).To(Succeed())
					Expect(res.GVK.IsEqualTo(gvk)).To(BeTrue())
					if options.Plural != "" {
						Expect(res.Plural).To(Equal(options.Plural))
					}
					if options.DoAPI || options.DoDefaulting || options.DoValidation || options.DoConversion {
						if multiGroup {
							Expect(res.Path).To(Equal(
								path.Join(cfg.GetRepository(), "api", gvk.Group, gvk.Version)))
						} else {
							Expect(res.Path).To(Equal(path.Join(cfg.GetRepository(), "api", gvk.Version)))
						}
					} else if len(options.ExternalAPIPath) > 0 {
						Expect(res.Path).To(Equal("testPath"))
					} else {
						// Core-resources have a path despite not having an API/Webhook but they are not tested here
						Expect(res.Path).To(Equal(""))
					}
					Expect(res.API).NotTo(BeNil())
					if options.DoAPI {
						Expect(res.API.Namespaced).To(Equal(options.Namespaced))
						Expect(res.API.IsEmpty()).To(BeFalse())
					} else {
						Expect(res.API.IsEmpty()).To(BeTrue())
					}
					Expect(res.Controller).To(Equal(options.DoController))
					Expect(res.Webhooks).NotTo(BeNil())
					if options.DoDefaulting || options.DoValidation || options.DoConversion {
						Expect(res.Webhooks.Defaulting).To(Equal(options.DoDefaulting))
						Expect(res.Webhooks.Validation).To(Equal(options.DoValidation))
						Expect(res.Webhooks.Conversion).To(Equal(options.DoConversion))
						Expect(res.Webhooks.Spoke).To(Equal(options.Spoke))
						Expect(res.Webhooks.IsEmpty()).To(BeFalse())
					} else {
						Expect(res.Webhooks.IsEmpty()).To(BeTrue())
					}

					if len(options.ExternalAPIPath) > 0 {
						Expect(res.External).To(BeTrue())
						Expect(res.Domain).To(Equal("test.io"))
					}

					Expect(res.QualifiedGroup()).To(Equal(gvk.Group + "." + gvk.Domain))
					Expect(res.PackageName()).To(Equal(gvk.Group))
					Expect(res.ImportAlias()).To(Equal(gvk.Group + gvk.Version))
				}
			},
			Entry("when updating nothing", Options{}),
			Entry("when updating the plural", Options{Plural: "mates"}),
			Entry("when updating the Controller", Options{DoController: true}),
			Entry("when updating with External API Path", Options{ExternalAPIPath: "testPath", ExternalAPIDomain: "test.io"}),
			Entry("when updating the API with setting webhooks params",
				Options{DoAPI: true, DoDefaulting: true, DoValidation: true, DoConversion: true}),
		)

		DescribeTable("should use core apis",
			func(group, qualified string) {
				options := Options{}
				for _, multiGroup := range []bool{false, true} {
					if multiGroup {
						Expect(cfg.SetMultiGroup()).To(Succeed())
					} else {
						Expect(cfg.ClearMultiGroup()).To(Succeed())
					}

					res := resource.Resource{
						GVK: resource.GVK{
							Group:   group,
							Domain:  domain,
							Version: version,
							Kind:    kind,
						},
						Plural:   "firstmates",
						API:      &resource.API{},
						Webhooks: &resource.Webhooks{},
					}

					options.UpdateResource(&res, cfg)
					Expect(res.Validate()).To(Succeed())

					Expect(res.Path).To(Equal(path.Join("k8s.io", "api", group, version)))
					Expect(res.HasAPI()).To(BeFalse())
					Expect(res.QualifiedGroup()).To(Equal(qualified))
				}
			},
			Entry("for `apps`", "apps", "apps"),
			Entry("for `authentication`", "authentication", "authentication.k8s.io"),
		)

		DescribeTable("should use core apis with project version 2",
			// This needs a separate test because project version 2 didn't store API and therefore
			// the `HasAPI` method of the resource obtained with `GetResource` will always return false.
			// Instead, the existence of a resource in the list means the API was scaffolded.
			func(group, qualified string) {
				cfg = cfgv3.New()
				_ = cfg.SetRepository("test")

				options := Options{}
				for _, multiGroup := range []bool{false, true} {
					if multiGroup {
						Expect(cfg.SetMultiGroup()).To(Succeed())
					} else {
						Expect(cfg.ClearMultiGroup()).To(Succeed())
					}

					res := resource.Resource{
						GVK: resource.GVK{
							Group:   group,
							Domain:  domain,
							Version: version,
							Kind:    kind,
						},
						Plural:   "firstmates",
						API:      &resource.API{},
						Webhooks: &resource.Webhooks{},
					}

					options.UpdateResource(&res, cfg)
					Expect(res.Validate()).To(Succeed())

					Expect(res.Path).To(Equal(path.Join("k8s.io", "api", group, version)))
					Expect(res.HasAPI()).To(BeFalse())
					Expect(res.QualifiedGroup()).To(Equal(qualified))
				}
			},
			Entry("for `apps`", "apps", "apps"),
			Entry("for `authentication`", "authentication", "authentication.k8s.io"),
		)
	})
})


================================================
FILE: pkg/plugins/golang/repository.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 golang

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"os/exec"

	"golang.org/x/tools/go/packages"
)

// module and goMod arg just enough of the output of `go mod edit -json` for our purposes
type goMod struct {
	Module module
}
type module struct {
	Path string
}

// findGoModulePath finds the path of the current module, if present.
func findGoModulePath() (string, error) {
	cmd := exec.Command("go", "mod", "edit", "-json")
	cmd.Env = append(cmd.Env, os.Environ()...)
	out, err := cmd.Output()
	if err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			err = fmt.Errorf("%s", string(exitErr.Stderr))
		}
		return "", err
	}
	mod := goMod{}
	if err = json.Unmarshal(out, &mod); err != nil {
		return "", fmt.Errorf("failed to unmarshal go.mod: %w", err)
	}
	return mod.Module.Path, nil
}

// FindCurrentRepo attempts to determine the current repository
// though a combination of go/packages and `go mod` commands/tricks.
func FindCurrentRepo() (string, error) {
	// easiest case: existing go module
	path, err := findGoModulePath()
	if err == nil {
		return path, nil
	}

	// next, check if we've got a package in the current directory
	pkgCfg := &packages.Config{
		Mode: packages.NeedName, // name gives us path as well
	}
	pkgs, err := packages.Load(pkgCfg, ".")
	// NB(directxman12): when go modules are off and we're outside GOPATH and
	// we don't otherwise have a good guess packages.Load will fabricate a path
	// that consists of `_/absolute/path/to/current/directory`.  We shouldn't
	// use that when it happens.
	if err == nil && len(pkgs) > 0 && len(pkgs[0].PkgPath) > 0 && pkgs[0].PkgPath[0] != '_' {
		return pkgs[0].PkgPath, nil
	}

	// otherwise, try to get `go mod init` to guess for us -- it's pretty good
	cmd := exec.Command("go", "mod", "init")
	cmd.Env = append(cmd.Env, os.Environ()...)
	if _, err := cmd.Output(); err != nil {
		var exitErr *exec.ExitError
		if errors.As(err, &exitErr) {
			err = fmt.Errorf("%s", string(exitErr.Stderr))
		}
		// give up, let the user figure it out
		return "", fmt.Errorf("could not determine repository path from module data, "+
			"package data, or by initializing a module: %w", err)
	}
	//nolint:errcheck
	defer os.Remove("go.mod") // clean up after ourselves
	return findGoModulePath()
}


================================================
FILE: pkg/plugins/golang/repository_test.go
================================================
/*
Copyright 2025 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 golang

import (
	"os"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("golang:repository", func() {
	var (
		tmpDir string
		oldDir string
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "repo-test")
		Expect(err).NotTo(HaveOccurred())
		oldDir, err = os.Getwd()
		Expect(err).NotTo(HaveOccurred())
		Expect(os.Chdir(tmpDir)).To(Succeed())
	})

	AfterEach(func() {
		Expect(os.Chdir(oldDir)).To(Succeed())
		Expect(os.RemoveAll(tmpDir)).To(Succeed())
	})

	When("go.mod exists", func() {
		BeforeEach(func() {
			// Simulate `go mod edit -json` output by writing a go.mod file and using go commands
			Expect(os.WriteFile("go.mod", []byte("module github.com/example/repo\n"), 0o644)).To(Succeed())
		})

		It("findGoModulePath returns the module path", func() {
			path, err := findGoModulePath()
			Expect(err).NotTo(HaveOccurred())
			Expect(path).To(Equal("github.com/example/repo"))
		})

		It("FindCurrentRepo returns the module path", func() {
			path, err := FindCurrentRepo()
			Expect(err).NotTo(HaveOccurred())
			Expect(path).To(Equal("github.com/example/repo"))
		})
	})

	When("go.mod does not exist", func() {
		It("findGoModulePath returns error", func() {
			got, err := findGoModulePath()
			Expect(err).To(HaveOccurred())
			Expect(got).To(Equal(""))
		})

		It("FindCurrentRepo tries to init a module and returns the path or a helpful error", func() {
			path, err := FindCurrentRepo()
			if err != nil {
				Expect(path).To(Equal(""))
				Expect(err.Error()).To(ContainSubstring("could not determine repository path"))
			} else {
				Expect(path).NotTo(BeEmpty())
			}
		})
	})

	When("go mod command fails with exec.ExitError", func() {
		var origPath string

		BeforeEach(func() {
			// Move go binary out of PATH to force exec error
			origPath = os.Getenv("PATH")
			// Set PATH to empty so "go" cannot be found
			Expect(os.Setenv("PATH", "")).To(Succeed())
		})

		AfterEach(func() {
			Expect(os.Setenv("PATH", origPath)).To(Succeed())
		})

		It("findGoModulePath returns error with stderr message", func() {
			got, err := findGoModulePath()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).NotTo(BeEmpty())
			Expect(got).To(Equal(""))
		})

		It("FindCurrentRepo returns error with stderr message", func() {
			got, err := FindCurrentRepo()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("could not determine repository path"))
			Expect(got).To(Equal(""))
		})
	})
})


================================================
FILE: pkg/plugins/golang/suite_test.go
================================================
/*
Copyright 2022 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 golang

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestGoPlugin(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Go Plugin Suite")
}


================================================
FILE: pkg/plugins/golang/v4/api.go
================================================
/*
Copyright 2022 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 v4

import (
	"bufio"
	"errors"
	"fmt"
	log "log/slog"
	"os"

	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
)

// DefaultMainPath is default file path of main.go
const DefaultMainPath = "cmd/main.go"

var _ plugin.CreateAPISubcommand = &createAPISubcommand{}

type createAPISubcommand struct {
	config config.Config

	options *goPlugin.Options

	resource *resource.Resource

	// Check if we have to scaffold resource and/or controller
	resourceFlag   *pflag.Flag
	controllerFlag *pflag.Flag

	// force indicates that the resource should be created even if it already exists
	force bool

	// runMake indicates whether to run make or not after scaffolding APIs
	runMake bool
}

func (p *createAPISubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = `Scaffold a Kubernetes API by writing a Resource definition and/or a Controller.

If information about whether the resource and controller should be scaffolded
was not explicitly provided, it will prompt the user if they should be.

After the scaffold is written, the dependencies will be updated and
make generate will be run.
`
	subcmdMeta.Examples = fmt.Sprintf(`  # Create a frigates API with Group: ship, Version: v1beta1 and Kind: Frigate
  %[1]s create api --group ship --version v1beta1 --kind Frigate

  # Edit the API Scheme

  nano api/v1beta1/frigate_types.go

  # Edit the Controller
  nano internal/controller/frigate/frigate_controller.go

  # Edit the Controller Test
  nano internal/controller/frigate/frigate_controller_test.go

  # Generate the manifests
  make manifests

  # Install CRDs into the Kubernetes cluster using kubectl apply
  make install

  # Regenerate code and run against the Kubernetes cluster configured by ~/.kube/config
  make run
`, cliMeta.CommandName)
}

func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) {
	fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files")

	fs.BoolVar(&p.force, "force", false,
		"attempt to create resource even if it already exists")

	p.options = &goPlugin.Options{}

	fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form")

	fs.BoolVar(&p.options.DoAPI, "resource", true,
		"if set, generate the resource without prompting the user")
	p.resourceFlag = fs.Lookup("resource")
	fs.BoolVar(&p.options.Namespaced, "namespaced", true, "resource is namespaced")

	fs.BoolVar(&p.options.DoController, "controller", true,
		"if set, generate the controller without prompting the user")
	p.controllerFlag = fs.Lookup("controller")

	fs.StringVar(&p.options.ExternalAPIPath, "external-api-path", "",
		"Specify the Go package import path for the external API. This is used to scaffold controllers for resources "+
			"defined outside this project (e.g., github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1).")

	fs.StringVar(&p.options.ExternalAPIDomain, "external-api-domain", "",
		"Specify the domain name for the external API. This domain is used to generate accurate RBAC "+
			"markers and permissions for the external resources (e.g., cert-manager.io).")

	fs.StringVar(&p.options.ExternalAPIModule, "external-api-module", "",
		"external API module with optional version (e.g., github.com/cert-manager/cert-manager@v1.18.2)")
}

func (p *createAPISubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *createAPISubcommand) InjectResource(res *resource.Resource) error {
	p.resource = res

	reader := bufio.NewReader(os.Stdin)
	if !p.resourceFlag.Changed {
		log.Info("Create Resource [y/n]")
		p.options.DoAPI = util.YesNo(reader)
	}
	if !p.controllerFlag.Changed {
		log.Info("Create Controller [y/n]")
		p.options.DoController = util.YesNo(reader)
	}

	// Ensure that external API options cannot be used when creating an API in the project.
	if p.options.DoAPI {
		if len(p.options.ExternalAPIPath) != 0 || len(p.options.ExternalAPIDomain) != 0 ||
			len(p.options.ExternalAPIModule) != 0 {
			return errors.New("cannot use '--external-api-path', '--external-api-domain', or '--external-api-module' " +
				"when creating an API in the project with '--resource=true'. " +
				"Use '--resource=false' when referencing an external API")
		}
	}

	// Validate that --external-api-module requires --external-api-path
	if len(p.options.ExternalAPIModule) != 0 && len(p.options.ExternalAPIPath) == 0 {
		return errors.New("'--external-api-module' requires '--external-api-path' to be specified")
	}

	p.options.UpdateResource(p.resource, p.config)

	if err := p.resource.Validate(); err != nil {
		return fmt.Errorf("error validating resource: %w", err)
	}

	// In case we want to scaffold a resource API we need to do some checks
	if p.options.DoAPI {
		// Check that resource doesn't have the API scaffolded or flag force was set
		if r, err := p.config.GetResource(p.resource.GVK); err == nil && r.HasAPI() && !p.force {
			return errors.New("API resource already exists")
		}

		// Check that the provided group can be added to the project
		if !p.config.IsMultiGroup() && p.config.ResourcesLength() != 0 && !p.config.HasGroup(p.resource.Group) {
			return fmt.Errorf("multiple groups are not allowed by default, " +
				"to enable multi-group visit https://kubebuilder.io/migration/multi-group.html")
		}
	}

	return nil
}

func (p *createAPISubcommand) PreScaffold(machinery.Filesystem) error {
	// check if main.go is present in the root directory
	if _, err := os.Stat(DefaultMainPath); os.IsNotExist(err) {
		return fmt.Errorf("%s file should present in the root directory", DefaultMainPath)
	}

	return nil
}

func (p *createAPISubcommand) Scaffold(fs machinery.Filesystem) error {
	scaffolder := scaffolds.NewAPIScaffolder(p.config, *p.resource, p.force)
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("error scaffolding API: %w", err)
	}

	return nil
}

func (p *createAPISubcommand) PostScaffold() error {
	// If external API with module specified, add it using go get
	if p.resource.IsExternal() && p.resource.Module != "" {
		log.Info("Adding external API dependency", "module", p.resource.Module)
		// Use go get to add the dependency cleanly as a direct requirement
		err := util.RunCmd("Add external API dependency", "go", "get", p.resource.Module)
		if err != nil {
			return fmt.Errorf("error adding external API dependency: %w", err)
		}
	}

	err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
	if err != nil {
		return fmt.Errorf("error updating go dependencies: %w", err)
	}
	if p.runMake && p.resource.HasAPI() {
		err = util.RunCmd("Running make", "make", "generate")
		if err != nil {
			return fmt.Errorf("error running make generate: %w", err)
		}
		fmt.Print("Next: implement your new API and generate the manifests (e.g. CRDs,CRs) with:\n$ make manifests\n")
	}

	return nil
}


================================================
FILE: pkg/plugins/golang/v4/api_test.go
================================================
/*
Copyright 2026 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 v4

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
	goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
)

var _ = Describe("createAPISubcommand", func() {
	var (
		subCmd *createAPISubcommand
		cfg    config.Config
		res    *resource.Resource
	)

	BeforeEach(func() {
		subCmd = &createAPISubcommand{}
		cfg = cfgv3.New()
		_ = cfg.SetRepository("github.com/example/test")

		subCmd.options = &goPlugin.Options{}
		subCmd.resourceFlag = &pflag.Flag{Changed: true}
		subCmd.controllerFlag = &pflag.Flag{Changed: true}

		res = &resource.Resource{
			GVK: resource.GVK{
				Group:   "crew",
				Domain:  "test.io",
				Version: "v1",
				Kind:    "Captain",
			},
			Plural:   "captains",
			API:      &resource.API{},
			Webhooks: &resource.Webhooks{},
		}

		Expect(subCmd.InjectConfig(cfg)).To(Succeed())
	})

	It("should reject external API options when creating API in project", func() {
		subCmd.options.DoAPI = true
		subCmd.options.ExternalAPIPath = "github.com/external/api"

		err := subCmd.InjectResource(res)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("cannot use '--external-api-path'"))
	})

	It("should require external-api-path when using external-api-module", func() {
		subCmd.options.DoAPI = false
		subCmd.options.ExternalAPIModule = "github.com/external/api@v1.0.0"
		subCmd.options.ExternalAPIPath = ""

		err := subCmd.InjectResource(res)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("requires '--external-api-path'"))
	})

	It("should prevent duplicate API without force flag", func() {
		subCmd.options.DoAPI = true
		subCmd.options.DoController = true

		resWithAPI := *res
		resWithAPI.API = &resource.API{CRDVersion: "v1"}
		Expect(cfg.AddResource(resWithAPI)).To(Succeed())

		subCmd.force = false
		err := subCmd.InjectResource(res)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("API resource already exists"))
	})

	It("should allow duplicate API with force flag", func() {
		subCmd.options.DoAPI = true
		subCmd.options.DoController = true

		resWithAPI := *res
		resWithAPI.API = &resource.API{CRDVersion: "v1"}
		Expect(cfg.AddResource(resWithAPI)).To(Succeed())

		subCmd.force = true
		err := subCmd.InjectResource(res)

		Expect(err).NotTo(HaveOccurred())
	})

	It("should prevent multiple groups in single-group project", func() {
		subCmd.options.DoAPI = true
		subCmd.options.DoController = true

		firstRes := resource.Resource{
			GVK: resource.GVK{
				Group:   "ship",
				Domain:  "test.io",
				Version: "v1",
				Kind:    "Frigate",
			},
			Plural: "frigates",
			API:    &resource.API{CRDVersion: "v1"},
		}
		Expect(cfg.AddResource(firstRes)).To(Succeed())

		res.Group = "crew"
		res.Plural = "captains"

		err := subCmd.InjectResource(res)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("multiple groups are not allowed"))
	})

	It("should allow multiple groups when multigroup is enabled", func() {
		subCmd.options.DoAPI = true
		subCmd.options.DoController = true

		Expect(cfg.SetMultiGroup()).To(Succeed())

		firstRes := resource.Resource{
			GVK: resource.GVK{
				Group:   "ship",
				Domain:  "test.io",
				Version: "v1",
				Kind:    "Frigate",
			},
			Plural: "frigates",
			API:    &resource.API{CRDVersion: "v1"},
		}
		Expect(cfg.AddResource(firstRes)).To(Succeed())

		res.Group = "crew"

		Expect(subCmd.InjectResource(res)).To(Succeed())
	})
})


================================================
FILE: pkg/plugins/golang/v4/edit.go
================================================
/*
Copyright 2022 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 v4

import (
	"fmt"

	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
)

var _ plugin.EditSubcommand = &editSubcommand{}

type editSubcommand struct {
	config config.Config

	multigroup bool
	namespaced bool
	force      bool

	// fs stores the FlagSet to check if flags were explicitly set
	fs *pflag.FlagSet
}

func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = `Edit project configuration to enable or disable layout settings.

Multigroup (--multigroup):
  Enable or disable multi-group layout.
  Changes API structure: api// becomes api///
  Automatic: Updates PROJECT file, future APIs use new structure
  Manual: Move existing API files, update import paths in controllers
  More info: https://book.kubebuilder.io/migration/multi-group.html

Namespaced (--namespaced):
  Enable or disable namespace-scoped deployment.
  Manager watches one or more specific namespaces vs all namespaces.
  Namespaces to watch are configured via WATCH_NAMESPACE environment variable.
  Automatic: Updates PROJECT file, scaffolds Role/RoleBinding, uses --force to regenerate manager.yaml
  Manual: Add namespace= to RBAC markers in existing controllers, update cmd/main.go, run 'make manifests'
  More info: https://book.kubebuilder.io/migration/namespace-scoped.html 
  
  WARNING - Webhooks and Namespace-Scoped Mode:
  Webhooks remain cluster-scoped even in namespace-scoped mode.
  The manager cache is restricted to WATCH_NAMESPACE, but webhooks receive requests
  from ALL namespaces. You must configure namespaceSelector or objectSelector to align
  webhook scope with the cache.

Force (--force):
  Overwrite existing scaffolded files to apply configuration changes.
  Example: With --namespaced, regenerates config/manager/manager.yaml to add WATCH_NAMESPACE env var.
  Warning: This overwrites default scaffold files; manual changes in those files may be lost.

Note: To add optional plugins after initialization, use 'kubebuilder edit --plugins '.
      Run 'kubebuilder edit --plugins --help' to see available plugins.
`
	subcmdMeta.Examples = fmt.Sprintf(`  # Enable multigroup layout
  %[1]s edit --multigroup

  # Enable namespace-scoped permissions
  %[1]s edit --namespaced

  # Enable with automatic file regeneration
  %[1]s edit --namespaced --force

  # Disable multigroup layout
  %[1]s edit --multigroup=false

  # Enable/disable multiple settings
  %[1]s edit --multigroup --namespaced --force
`, cliMeta.CommandName)
}

func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
	p.fs = fs
	fs.BoolVar(&p.multigroup, "multigroup", false, "enable or disable multigroup layout")
	fs.BoolVar(&p.namespaced, "namespaced", false, "enable or disable namespace-scoped deployment")
	fs.BoolVar(&p.force, "force", false, "overwrite scaffolded files to apply changes (manual edits may be lost)")
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
	p.config = c

	return nil
}

func (p *editSubcommand) PreScaffold(machinery.Filesystem) error {
	// If flags were not explicitly set, preserve existing PROJECT file values
	// This prevents one flag from clearing another when using default values
	if !p.fs.Changed("multigroup") {
		p.multigroup = p.config.IsMultiGroup()
	}
	if !p.fs.Changed("namespaced") {
		p.namespaced = p.config.IsNamespaced()
	}

	return nil
}

func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
	scaffolder := scaffolds.NewEditScaffolder(p.config, p.multigroup, p.namespaced, p.force)
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("failed to edit scaffold: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/golang/v4/edit_test.go
================================================
/*
Copyright 2026 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 v4

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"
	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("editSubcommand", func() {
	var (
		subCmd *editSubcommand
		cfg    config.Config
	)

	BeforeEach(func() {
		subCmd = &editSubcommand{}
		cfg = cfgv3.New()
	})

	It("should inject config successfully", func() {
		Expect(subCmd.InjectConfig(cfg)).To(Succeed())
		Expect(subCmd.config).To(Equal(cfg))
	})

	Context("PreScaffold", func() {
		var (
			fs     *pflag.FlagSet
			mockFS machinery.Filesystem
		)

		BeforeEach(func() {
			fs = pflag.NewFlagSet("test", pflag.ContinueOnError)
			subCmd.BindFlags(fs)
			Expect(subCmd.InjectConfig(cfg)).To(Succeed())
			mockFS = machinery.Filesystem{FS: afero.NewMemMapFs()}
		})

		It("should preserve existing multigroup setting when only namespaced flag is set", func() {
			// Set multigroup in PROJECT file
			Expect(cfg.SetMultiGroup()).To(Succeed())
			Expect(cfg.IsMultiGroup()).To(BeTrue())

			// Only set namespaced flag (multigroup not set, so it defaults to false)
			Expect(fs.Set("namespaced", "true")).To(Succeed())

			// PreScaffold should preserve the existing multigroup value
			Expect(subCmd.PreScaffold(mockFS)).To(Succeed())

			// Both should be true
			Expect(subCmd.multigroup).To(BeTrue(), "multigroup should be preserved from PROJECT file")
			Expect(subCmd.namespaced).To(BeTrue(), "namespaced should be set from flag")
		})

		It("should preserve existing namespaced setting when only multigroup flag is set", func() {
			// Set namespaced in PROJECT file
			Expect(cfg.SetNamespaced()).To(Succeed())
			Expect(cfg.IsNamespaced()).To(BeTrue())

			// Only set multigroup flag (namespaced not set, so it defaults to false)
			Expect(fs.Set("multigroup", "true")).To(Succeed())

			// PreScaffold should preserve the existing namespaced value
			Expect(subCmd.PreScaffold(mockFS)).To(Succeed())

			// Both should be true
			Expect(subCmd.multigroup).To(BeTrue(), "multigroup should be set from flag")
			Expect(subCmd.namespaced).To(BeTrue(), "namespaced should be preserved from PROJECT file")
		})

		It("should allow explicitly disabling a flag", func() {
			// Set both in PROJECT file
			Expect(cfg.SetMultiGroup()).To(Succeed())
			Expect(cfg.SetNamespaced()).To(Succeed())

			// Explicitly disable multigroup
			Expect(fs.Set("multigroup", "false")).To(Succeed())

			// PreScaffold should respect the explicit false
			Expect(subCmd.PreScaffold(mockFS)).To(Succeed())

			// multigroup should be false (explicitly set), namespaced should be true (from PROJECT)
			Expect(subCmd.multigroup).To(BeFalse(), "multigroup should be explicitly disabled")
			Expect(subCmd.namespaced).To(BeTrue(), "namespaced should be preserved from PROJECT file")
		})

		It("should use flag values when both flags are explicitly set", func() {
			// Set different values in PROJECT file
			Expect(cfg.SetMultiGroup()).To(Succeed())
			Expect(cfg.ClearNamespaced()).To(Succeed())

			// Explicitly set both flags to opposite values
			Expect(fs.Set("multigroup", "false")).To(Succeed())
			Expect(fs.Set("namespaced", "true")).To(Succeed())

			// PreScaffold should use the explicit flag values
			Expect(subCmd.PreScaffold(mockFS)).To(Succeed())

			Expect(subCmd.multigroup).To(BeFalse(), "multigroup should use explicit flag value")
			Expect(subCmd.namespaced).To(BeTrue(), "namespaced should use explicit flag value")
		})

		It("should preserve PROJECT file values when no flags are set", func() {
			// Set values in PROJECT file
			Expect(cfg.SetMultiGroup()).To(Succeed())
			Expect(cfg.SetNamespaced()).To(Succeed())

			// Don't set any flags

			// PreScaffold should preserve both values from PROJECT file
			Expect(subCmd.PreScaffold(mockFS)).To(Succeed())

			Expect(subCmd.multigroup).To(BeTrue(), "multigroup should be preserved from PROJECT file")
			Expect(subCmd.namespaced).To(BeTrue(), "namespaced should be preserved from PROJECT file")
		})
	})
})


================================================
FILE: pkg/plugins/golang/v4/init.go
================================================
/*
Copyright 2022 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 v4

import (
	"fmt"
	log "log/slog"
	"os"
	"path/filepath"
	"slices"
	"strings"

	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
)

// Variables and function to check Go version requirements.
var (
	goVerMin = golang.MustParse("go1.23.0")
	goVerMax = golang.MustParse("go2.0alpha1")
)

var _ plugin.InitSubcommand = &initSubcommand{}

type initSubcommand struct {
	config config.Config
	// For help text.
	commandName string

	// boilerplate options
	license string
	owner   string

	// go config options
	repo string

	// flags
	fetchDeps          bool
	skipGoVersionCheck bool
	multigroup         bool
	namespaced         bool
}

func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	p.commandName = cliMeta.CommandName

	subcmdMeta.Description = `Initialize a new project including the following files:
  - a "go.mod" with project dependencies
  - a "PROJECT" file that stores project configuration
  - a "Makefile" with several useful make targets for the project
  - several YAML files for project deployment under the "config" directory
  - a "cmd/main.go" file that creates the manager that will run the project controllers

Required flags:
  --domain: Domain for your APIs (e.g., example.org creates crew.example.org for API groups)

Configuration flags:
  --repo: Go module path (e.g., github.com/user/repo); auto-detected if not provided
  --owner: Owner name for copyright license headers
  --license: License to use (apache2 or none, default: apache2)

Plugin flags:
  --plugins: Comma-separated list of plugins to use (default: go/v4)
             Plugins scaffold files during init and are saved to the PROJECT layout
             Future operations (i.e. create api, create webhook) call all plugins in the chain
             Run 'kubebuilder init --plugins --help' to see available plugins

Layout flags:
  --multigroup: Enable multigroup layout to organize APIs by group
                Scaffolds APIs in api/// instead of api//
                Useful when managing multiple API groups (e.g., batch, apps, crew)
  --namespaced: Enable namespace-scoped deployment instead of cluster-scoped
                Manager watches one or more specific namespaces instead of all namespaces
                Namespaces to watch are configured via WATCH_NAMESPACE environment variable
                Uses Role/RoleBinding instead of ClusterRole/ClusterRoleBinding
                Suitable for multi-tenant environments or limited scope deployments

Note: Layout settings can be changed later with 'kubebuilder edit'.
`
	subcmdMeta.Examples = fmt.Sprintf(`  # Initialize a new project
  %[1]s init --domain example.org

  # Initialize with multigroup layout
  %[1]s init --domain example.org --multigroup

  # Initialize with namespace-scoped deployment
  %[1]s init --domain example.org --namespaced

  # Initialize with optional plugins
  %[1]s init --plugins go/v4,autoupdate/v1-alpha --domain example.org
  %[1]s init --plugins go/v4,helm/v2-alpha --domain example.org

  # Initialize with custom settings
  %[1]s init --domain example.org --owner "Your Name" --license apache2

  # Initialize with all options combined
  %[1]s init --plugins go/v4,autoupdate/v1-alpha --domain example.org --multigroup --namespaced
`, cliMeta.CommandName)
}

func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
	fs.BoolVar(&p.skipGoVersionCheck, "skip-go-version-check",
		false, "skip Go version check")

	// dependency args
	fs.BoolVar(&p.fetchDeps, "fetch-deps", true, "download dependencies after scaffolding")

	// boilerplate args
	fs.StringVar(&p.license, "license", "apache2",
		"license header to use (apache2 or none)")
	fs.StringVar(&p.owner, "owner", "", "copyright owner for license headers")

	// project args
	fs.StringVar(&p.repo, "repo", "", "Go module name (e.g., github.com/user/repo); "+
		"auto-detected from current directory if not provided")
	fs.BoolVar(&p.multigroup, "multigroup", false,
		"enable multigroup layout (organize APIs by group)")
	fs.BoolVar(&p.namespaced, "namespaced", false,
		"enable namespace-scoped deployment (default: cluster-scoped)")
}

func (p *initSubcommand) InjectConfig(c config.Config) error {
	p.config = c

	// Try to guess repository if flag is not set.
	if p.repo == "" {
		repoPath, err := golang.FindCurrentRepo()
		if err != nil {
			return fmt.Errorf("error finding current repository: %w", err)
		}
		p.repo = repoPath
	}

	if err := p.config.SetRepository(p.repo); err != nil {
		return fmt.Errorf("error setting repository: %w", err)
	}

	if p.multigroup {
		if err := p.config.SetMultiGroup(); err != nil {
			return fmt.Errorf("error setting multigroup: %w", err)
		}
	}

	if p.namespaced {
		if err := p.config.SetNamespaced(); err != nil {
			return fmt.Errorf("error setting namespaced: %w", err)
		}
	}

	return nil
}

func (p *initSubcommand) PreScaffold(machinery.Filesystem) error {
	// Ensure Go version is in the allowed range if check not turned off.
	if !p.skipGoVersionCheck {
		if err := golang.ValidateGoVersion(goVerMin, goVerMax); err != nil {
			return fmt.Errorf("error validating go version: %w", err)
		}
	}

	// Check if the current directory has no files or directories which does not allow to init the project
	return checkDir()
}

func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
	scaffolder := scaffolds.NewInitScaffolder(p.config, p.license, p.owner, p.commandName)
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("error scaffolding init plugin: %w", err)
	}

	if !p.fetchDeps {
		log.Info("skipping fetching dependencies")
		return nil
	}

	// Ensure that we are pinning controller-runtime version
	// xref: https://github.com/kubernetes-sigs/kubebuilder/issues/997
	err := util.RunCmd("Get controller runtime", "go", "get",
		"sigs.k8s.io/controller-runtime@"+scaffolds.ControllerRuntimeVersion)
	if err != nil {
		return fmt.Errorf("error getting controller-runtime version: %w", err)
	}

	return nil
}

func (p *initSubcommand) PostScaffold() error {
	err := util.RunCmd("Update dependencies", "go", "mod", "tidy")
	if err != nil {
		return fmt.Errorf("error updating go dependencies: %w", err)
	}

	fmt.Printf("Next: define a resource with:\n$ %s create api\n", p.commandName)
	return nil
}

// checkDir checks the target directory before scaffolding:
// 1. Returns error if key kubebuilder files already exist (prevents re-initialization)
// 2. Warns if directory is not empty (but allows scaffolding to continue)
func checkDir() error {
	// Files scaffolded by 'kubebuilder init' that indicate the directory is already initialized.
	// Blocking these prevents accidental re-initialization and file conflicts.
	// Note: go.mod and go.sum are NOT blocked because:
	//   - They may exist in pre-existing Go projects
	//   - Kubebuilder will overwrite them (machinery.OverwriteFile)
	//   - Testdata generation creates go.mod before running init
	scaffoldedFiles := []string{
		"PROJECT",                       // Kubebuilder project config (key indicator)
		"Makefile",                      // Build automation
		filepath.Join("cmd", "main.go"), // Controller manager entry point
	}

	// Check for existing scaffolded files
	for _, file := range scaffoldedFiles {
		if _, err := os.Stat(file); err == nil {
			return fmt.Errorf("target directory is already initialized. "+
				"Found existing kubebuilder file %q. "+
				"Please run this command in a new directory or remove existing scaffolded files", file)
		}
	}

	// Check if directory has any other files (warn only)
	// Note: We ignore certain files that are expected or safely overwritten:
	//   - go.mod and go.sum: Users may run `go mod init` before `kubebuilder init`
	//   - .gitignore and .dockerignore: Safely overwritten by kubebuilder
	//   - Other dot directories (.git, .vscode, .idea): Not scaffolded by kubebuilder
	// However, we DO check .github directory since kubebuilder scaffolds workflows there
	var hasFiles bool
	err := filepath.Walk(".",
		func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return fmt.Errorf("error walking path %q: %w", path, err)
			}
			// Skip the current directory itself
			if path == "." {
				return nil
			}
			// Skip dot directories EXCEPT .github (which contains scaffolded workflows)
			if info.IsDir() && strings.HasPrefix(info.Name(), ".") {
				if info.Name() != ".github" {
					return filepath.SkipDir
				}
			}
			// Skip files that are expected or safely overwritten
			ignoredFiles := []string{"go.mod", "go.sum"}
			if slices.Contains(ignoredFiles, info.Name()) {
				return nil
			}
			// Track if any other files/directories exist
			hasFiles = true
			return nil
		})
	if err != nil {
		return fmt.Errorf("error walking directory: %w", err)
	}

	// Warn if directory is not empty (but don't block)
	if hasFiles {
		log.Warn("The target directory is not empty. " +
			"Scaffolding may overwrite existing files or cause conflicts. " +
			"It is recommended to initialize in an empty directory.")
	}

	return nil
}


================================================
FILE: pkg/plugins/golang/v4/init_test.go
================================================
/*
Copyright 2026 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 v4

import (
	"os"
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
)

const testRepo = "github.com/example/test"

var _ = Describe("initSubcommand", func() {
	var (
		subCmd *initSubcommand
		cfg    config.Config
	)

	BeforeEach(func() {
		subCmd = &initSubcommand{}
		cfg = cfgv3.New()
	})

	Context("InjectConfig", func() {
		It("should set repository when provided", func() {
			subCmd.repo = testRepo
			err := subCmd.InjectConfig(cfg)

			Expect(err).NotTo(HaveOccurred())
			Expect(cfg.GetRepository()).To(Equal(testRepo))
		})

		It("should fail when repository cannot be detected", func() {
			originalDir, err := os.Getwd()
			Expect(err).NotTo(HaveOccurred())
			defer func() { _ = os.Chdir(originalDir) }()

			tmpDir, err := os.MkdirTemp("", "test-init")
			Expect(err).NotTo(HaveOccurred())
			defer func() { _ = os.RemoveAll(tmpDir) }()

			err = os.Chdir(tmpDir)
			Expect(err).NotTo(HaveOccurred())

			subCmd.repo = ""
			err = subCmd.InjectConfig(cfg)

			Expect(err).To(HaveOccurred())
		})

		It("should set multigroup when flag is enabled", func() {
			subCmd.repo = testRepo
			subCmd.multigroup = true
			err := subCmd.InjectConfig(cfg)

			Expect(err).NotTo(HaveOccurred())
			Expect(cfg.IsMultiGroup()).To(BeTrue())
		})

		It("should not set multigroup when flag is disabled", func() {
			subCmd.repo = testRepo
			subCmd.multigroup = false
			err := subCmd.InjectConfig(cfg)

			Expect(err).NotTo(HaveOccurred())
			Expect(cfg.IsMultiGroup()).To(BeFalse())
		})

		It("should set namespaced when flag is enabled", func() {
			subCmd.repo = testRepo
			subCmd.namespaced = true
			err := subCmd.InjectConfig(cfg)

			Expect(err).NotTo(HaveOccurred())
			Expect(cfg.IsNamespaced()).To(BeTrue())
		})

		It("should set both multigroup and namespaced when both flags are enabled", func() {
			subCmd.repo = testRepo
			subCmd.multigroup = true
			subCmd.namespaced = true
			err := subCmd.InjectConfig(cfg)

			Expect(err).NotTo(HaveOccurred())
			Expect(cfg.IsMultiGroup()).To(BeTrue())
			Expect(cfg.IsNamespaced()).To(BeTrue())
		})
	})

	Context("checkDir validation", func() {
		var tmpDir string

		BeforeEach(func() {
			var err error
			tmpDir, err = os.MkdirTemp("", "test-checkdir")
			Expect(err).NotTo(HaveOccurred())

			originalDir, err := os.Getwd()
			Expect(err).NotTo(HaveOccurred())
			DeferCleanup(func() {
				_ = os.Chdir(originalDir)
				_ = os.RemoveAll(tmpDir)
			})

			err = os.Chdir(tmpDir)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should pass for empty directory", func() {
			Expect(checkDir()).To(Succeed())
		})

		It("should pass when only go.mod exists", func() {
			err := os.WriteFile("go.mod", []byte("module test"), 0o644)
			Expect(err).NotTo(HaveOccurred())

			Expect(checkDir()).To(Succeed())
		})

		It("should fail when PROJECT already exists", func() {
			err := os.WriteFile("PROJECT", []byte("version: 3"), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = checkDir()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("already initialized"))
		})

		It("should fail when Makefile exists", func() {
			err := os.WriteFile("Makefile", []byte("all:"), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = checkDir()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("already initialized"))
		})

		It("should fail when cmd/main.go exists", func() {
			err := os.MkdirAll("cmd", 0o755)
			Expect(err).NotTo(HaveOccurred())

			err = os.WriteFile(filepath.Join("cmd", "main.go"), []byte("package main"), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = checkDir()
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("already initialized"))
		})
	})
})


================================================
FILE: pkg/plugins/golang/v4/plugin.go
================================================
/*
Copyright 2022 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 v4

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
)

const pluginName = "base." + golang.DefaultNameQualifier

var (
	pluginVersion            = plugin.Version{Number: 4, Stage: stage.Stable}
	supportedProjectVersions = []config.Version{cfgv3.Version}
)

var _ plugin.Full = Plugin{}

// Plugin implements the plugin.Full interface
type Plugin struct {
	initSubcommand
	createAPISubcommand
	createWebhookSubcommand
	editSubcommand
}

// Name returns the name of the plugin
func (Plugin) Name() string { return pluginName }

// Version returns the version of the plugin
func (Plugin) Version() plugin.Version { return pluginVersion }

// SupportedProjectVersions returns an array with all project versions supported by the plugin
func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }

// GetInitSubcommand will return the subcommand which is responsible for initializing and common scaffolding
func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }

// GetCreateAPISubcommand will return the subcommand which is responsible for scaffolding apis
func (p Plugin) GetCreateAPISubcommand() plugin.CreateAPISubcommand { return &p.createAPISubcommand }

// GetCreateWebhookSubcommand will return the subcommand which is responsible for scaffolding webhooks
func (p Plugin) GetCreateWebhookSubcommand() plugin.CreateWebhookSubcommand {
	return &p.createWebhookSubcommand
}

// GetEditSubcommand will return the subcommand which is responsible for editing the scaffold of the project
func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }

// Description returns a short description of the plugin
func (Plugin) Description() string {
	return "Default scaffold (go/v4 + kustomize/v2)"
}

// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
func (p Plugin) DeprecationWarning() string {
	return ""
}


================================================
FILE: pkg/plugins/golang/v4/plugin_test.go
================================================
/*
Copyright 2026 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 v4

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
)

var _ = Describe("Plugin", func() {
	var p Plugin

	It("should have correct version and support v3 projects", func() {
		Expect(p.Version().Number).To(Equal(4))
		Expect(p.SupportedProjectVersions()).To(ContainElement(cfgv3.Version))
	})

	It("should not be deprecated", func() {
		Expect(p.DeprecationWarning()).To(BeEmpty())
	})
})


================================================
FILE: pkg/plugins/golang/v4/scaffolds/api.go
================================================
/*
Copyright 2022 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 scaffolds

import (
	"errors"
	"fmt"
	log "log/slog"

	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/api"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/controllers"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack"
)

var _ plugins.Scaffolder = &apiScaffolder{}

// apiScaffolder contains configuration for generating scaffolding for Go type
// representing the API and controller that implements the behavior for the API.
type apiScaffolder struct {
	config   config.Config
	resource resource.Resource

	// fs is the filesystem that will be used by the scaffolder
	fs machinery.Filesystem

	// force indicates whether to scaffold controller files even if it exists or not
	force bool
}

// NewAPIScaffolder returns a new Scaffolder for API/controller creation operations
func NewAPIScaffolder(cfg config.Config, res resource.Resource, force bool) plugins.Scaffolder {
	return &apiScaffolder{
		config:   cfg,
		resource: res,
		force:    force,
	}
}

// InjectFS implements cmdutil.Scaffolder
func (s *apiScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// Scaffold implements cmdutil.Scaffolder
func (s *apiScaffolder) Scaffold() error {
	log.Info("Writing scaffold for you to edit...")

	// Load the boilerplate
	boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
	if err != nil {
		if errors.Is(err, afero.ErrFileNotFound) {
			log.Warn("unable to find boilerplate file. "+
				"This file is used to generate the license header in the project.\n"+
				"Note that controller-gen will also use this. Ensure that you "+
				"add the license file or configure your project accordingly",
				"file_path", hack.DefaultBoilerplatePath, "error", err)
			boilerplate = []byte("")
		} else {
			return fmt.Errorf("error scaffolding API/controller: failed to load boilerplate: %w", err)
		}
	}

	// Initialize the machinery.Scaffold that will write the files to disk
	scaffold := machinery.NewScaffold(s.fs,
		machinery.WithConfig(s.config),
		machinery.WithBoilerplate(string(boilerplate)),
		machinery.WithResource(&s.resource),
	)

	// Keep track of these values before the update
	doAPI := s.resource.HasAPI()
	doController := s.resource.HasController()

	if err := s.config.UpdateResource(s.resource); err != nil {
		return fmt.Errorf("error updating resource: %w", err)
	}

	if doAPI {
		if err := scaffold.Execute(
			&api.Types{Force: s.force},
			&api.Group{},
		); err != nil {
			return fmt.Errorf("error scaffolding APIs: %w", err)
		}
	}

	if doController {
		if err := scaffold.Execute(
			&controllers.SuiteTest{Force: s.force},
			&controllers.Controller{ControllerRuntimeVersion: ControllerRuntimeVersion, Force: s.force},
			&controllers.ControllerTest{Force: s.force, DoAPI: doAPI},
		); err != nil {
			return fmt.Errorf("error scaffolding controller: %w", err)
		}
	}

	if err := scaffold.Execute(
		&cmd.MainUpdater{WireResource: doAPI, WireController: doController},
	); err != nil {
		return fmt.Errorf("error updating cmd/main.go: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/doc.go
================================================
/*
Copyright 2022 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 scaffolds contains libraries for scaffolding code to use with controller-runtime
package scaffolds


================================================
FILE: pkg/plugins/golang/v4/scaffolds/edit.go
================================================
/*
Copyright 2022 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 scaffolds

import (
	"fmt"
	log "log/slog"

	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2/scaffolds"
)

var _ plugins.Scaffolder = &editScaffolder{}

type editScaffolder struct {
	config     config.Config
	multigroup bool
	namespaced bool
	force      bool

	// fs is the filesystem that will be used by the scaffolder
	fs machinery.Filesystem
}

// NewEditScaffolder returns a new Scaffolder for configuration edit operations
func NewEditScaffolder(cfg config.Config, multigroup bool, namespaced bool, force bool) plugins.Scaffolder {
	return &editScaffolder{
		config:     cfg,
		multigroup: multigroup,
		namespaced: namespaced,
		force:      force,
	}
}

// InjectFS implements cmdutil.Scaffolder
func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// Scaffold implements cmdutil.Scaffolder
func (s *editScaffolder) Scaffold() error {
	filename := "Dockerfile"
	bs, err := afero.ReadFile(s.fs.FS, filename)
	if err != nil {
		return fmt.Errorf("error reading %q: %w", filename, err)
	}
	str := string(bs)

	// Track if we're toggling namespaced mode
	wasNamespaced := s.config.IsNamespaced()

	// Update config flags
	if s.multigroup {
		_ = s.config.SetMultiGroup()
	} else {
		_ = s.config.ClearMultiGroup()
	}

	if s.namespaced {
		_ = s.config.SetNamespaced()
	} else {
		_ = s.config.ClearNamespaced()
	}

	// Scaffold appropriate RBAC and manager config based on namespaced flag
	if s.namespaced && !wasNamespaced {
		// Switching to namespaced layout: scaffold Role/RoleBinding and WATCH_NAMESPACE
		if rbacErr := s.scaffoldNamespacedRBAC(s.force); rbacErr != nil {
			return fmt.Errorf("failed to scaffold namespaced RBAC: %w", rbacErr)
		}

		if !s.force {
			fmt.Println()
			fmt.Println("Run with --force to update config/manager/manager.yaml with WATCH_NAMESPACE")
		}

		// Check if project has webhooks and warn about scope mismatch
		if s.hasWebhooks() {
			log.Warn("your project has webhooks which are cluster-scoped.\n" +
				"You will need to manually configure namespaceSelector or objectSelector")
		}

		// Print next steps
		fmt.Println()
		fmt.Println("Next steps:")
		fmt.Println("1. Update cmd/main.go to configure namespace-scoped cache")
		fmt.Println("2. Add namespace= to RBAC markers in existing controllers:")
		fmt.Printf("   // +kubebuilder:rbac:groups=mygroup,resources=myresources,verbs=get;list,"+
			"namespace=%s-system\n", s.config.GetProjectName())
		fmt.Println("3. Run: make manifests")

		if s.hasWebhooks() {
			fmt.Println("4. Configure namespaceSelector or objectSelector for webhooks")
		}

		fmt.Println()
		fmt.Println("See: https://book.kubebuilder.io/migration/namespace-scoped.html")
	} else if !s.namespaced && wasNamespaced {
		// Switching to cluster-scoped layout: scaffold ClusterRole/ClusterRoleBinding
		if rbacErr := s.scaffoldClusterRBAC(s.force); rbacErr != nil {
			return fmt.Errorf("failed to scaffold cluster-scoped RBAC: %w", rbacErr)
		}

		if !s.force {
			fmt.Println()
			fmt.Println("Run with --force to update config/manager/manager.yaml (remove WATCH_NAMESPACE)")
		}

		// Print next steps
		fmt.Println()
		fmt.Println("Next steps:")
		fmt.Println("1. Update cmd/main.go:")
		fmt.Println("   - Remove getWatchNamespace() and setupCacheNamespaces() functions")
		fmt.Println("   - Remove watchNamespace retrieval and cache configuration")
		fmt.Println("2. Remove namespace= from RBAC markers in existing controllers")
		fmt.Println("3. Run: make manifests")
		fmt.Println()
		fmt.Println("See: https://book.kubebuilder.io/migration/namespace-scoped.html")
	}

	// Check if the str is not empty, because when the file is already in desired format it will return empty string
	// because there is nothing to replace.
	if str != "" {
		// TODO: instead of writing it directly, we should use the scaffolding machinery for consistency
		if err = afero.WriteFile(s.fs.FS, filename, []byte(str), 0o644); err != nil {
			return fmt.Errorf("error writing %q: %w", filename, err)
		}
	}

	return nil
}

func (s *editScaffolder) scaffoldNamespacedRBAC(force bool) error {
	// Use the kustomize/v2 scaffolder to scaffold namespace-scoped RBAC and manager config
	rbacScaffolder := kustomizecommonv2.NewEditScaffolder(s.config, true, force)
	rbacScaffolder.InjectFS(s.fs)
	if err := rbacScaffolder.Scaffold(); err != nil {
		return fmt.Errorf("failed to scaffold RBAC: %w", err)
	}
	return nil
}

func (s *editScaffolder) scaffoldClusterRBAC(force bool) error {
	// Use the kustomize/v2 scaffolder to scaffold cluster-scoped RBAC and manager config
	rbacScaffolder := kustomizecommonv2.NewEditScaffolder(s.config, false, force)
	rbacScaffolder.InjectFS(s.fs)
	if err := rbacScaffolder.Scaffold(); err != nil {
		return fmt.Errorf("failed to scaffold RBAC: %w", err)
	}
	return nil
}

// hasWebhooks checks if any resources in the project have webhooks configured
func (s *editScaffolder) hasWebhooks() bool {
	resources, err := s.config.GetResources()
	if err != nil {
		return false
	}
	for _, res := range resources {
		if res.Webhooks != nil && !res.Webhooks.IsEmpty() {
			return true
		}
	}
	return false
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/edit_integration_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

var _ = Describe("Edit Scaffolding Integration Test", func() {
	var kbc *utils.TestContext

	BeforeEach(func() {
		var err error
		kbc, err = utils.NewTestContext(pluginutil.KubebuilderBinName, "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())
	})

	AfterEach(func() {
		kbc.Destroy()
	})

	It("should handle scope transitions comprehensively", func() {
		roleFile := filepath.Join(kbc.Dir, "config", "rbac", "role.yaml")
		roleBindingFile := filepath.Join(kbc.Dir, "config", "rbac", "role_binding.yaml")
		managerFile := filepath.Join(kbc.Dir, "config", "manager", "manager.yaml")
		projectFile := filepath.Join(kbc.Dir, "PROJECT")

		// ========== Part 1: Cluster-scoped → Namespaced (without --force) ==========
		By("initializing a cluster-scoped project")
		err := kbc.Init(
			"--plugins", "go/v4",
			"--project-version", "3",
			"--domain", kbc.Domain,
		)
		Expect(err).NotTo(HaveOccurred())

		By("verifying initial state is cluster-scoped")
		content, err := os.ReadFile(roleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: ClusterRole"))

		By("enabling namespaced layout without --force")
		err = kbc.Edit("--namespaced")
		Expect(err).NotTo(HaveOccurred())

		By("verifying PROJECT file was updated")
		content, err = os.ReadFile(projectFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(strings.Split(string(content), "resources:")[0]).To(ContainSubstring("namespaced: true"))

		By("verifying RBAC was changed to namespace-scoped")
		content, err = os.ReadFile(roleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))
		Expect(string(content)).NotTo(ContainSubstring("kind: ClusterRole"))

		content, err = os.ReadFile(roleBindingFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: RoleBinding"))

		By("verifying manager.yaml was NOT updated (no --force)")
		content, err = os.ReadFile(managerFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).NotTo(ContainSubstring("WATCH_NAMESPACE"),
			"manager.yaml should not be updated without --force flag")

		// ========== Part 2: Revert to cluster, then switch with --force ==========
		By("reverting to cluster-scoped first")
		err = kbc.Edit("--namespaced=false")
		Expect(err).NotTo(HaveOccurred())

		By("re-enabling namespaced layout with --force to update manager.yaml")
		err = kbc.Edit("--namespaced", "--force")
		Expect(err).NotTo(HaveOccurred())

		By("verifying manager.yaml was updated with WATCH_NAMESPACE")
		content, err = os.ReadFile(managerFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("- name: WATCH_NAMESPACE"))
		Expect(string(content)).To(ContainSubstring("fieldRef:"))
		Expect(string(content)).To(ContainSubstring("fieldPath: metadata.namespace"))

		// ========== Part 3: Create API and verify namespace RBAC markers ==========
		By("creating an API in namespaced mode")
		err = kbc.CreateAPI(
			"--group", kbc.Group,
			"--version", kbc.Version,
			"--kind", kbc.Kind,
			"--resource",
			"--controller",
			"--make=false",
		)
		Expect(err).NotTo(HaveOccurred())

		By("verifying controller has namespace parameter in RBAC markers")
		controllerFile := filepath.Join(kbc.Dir, "internal", "controller",
			strings.ToLower(kbc.Kind)+"_controller.go")
		content, err = os.ReadFile(controllerFile)
		Expect(err).NotTo(HaveOccurred())
		expectedNamespace := "e2e-" + kbc.TestSuffix + "-system"
		Expect(string(content)).To(ContainSubstring("namespace="+expectedNamespace),
			"Controller RBAC markers should include namespace parameter")

		By("verifying CRD admin/editor/viewer roles are namespace-scoped")
		adminRoleFile := filepath.Join(kbc.Dir, "config", "rbac",
			strings.ToLower(kbc.Kind)+"_admin_role.yaml")
		editorRoleFile := filepath.Join(kbc.Dir, "config", "rbac",
			strings.ToLower(kbc.Kind)+"_editor_role.yaml")
		viewerRoleFile := filepath.Join(kbc.Dir, "config", "rbac",
			strings.ToLower(kbc.Kind)+"_viewer_role.yaml")

		content, err = os.ReadFile(adminRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))

		content, err = os.ReadFile(editorRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))

		content, err = os.ReadFile(viewerRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))

		// ========== Part 4: Switch to cluster-scoped and verify all roles updated ==========
		By("switching to cluster-scoped with --force")
		err = kbc.Edit("--namespaced=false", "--force")
		Expect(err).NotTo(HaveOccurred())

		By("verifying PROJECT file was updated")
		content, err = os.ReadFile(projectFile)
		Expect(err).NotTo(HaveOccurred())
		projectContent := string(content)
		beforeResources := strings.Split(projectContent, "resources:")[0]
		if strings.Contains(beforeResources, "namespaced:") {
			Expect(beforeResources).To(ContainSubstring("namespaced: false"))
		}

		By("verifying manager RBAC was changed to cluster-scoped")
		content, err = os.ReadFile(roleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: ClusterRole"))

		content, err = os.ReadFile(roleBindingFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: ClusterRoleBinding"))

		By("verifying manager.yaml was updated (WATCH_NAMESPACE removed)")
		content, err = os.ReadFile(managerFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).NotTo(ContainSubstring("WATCH_NAMESPACE"))

		By("verifying CRD roles were updated to cluster-scoped")
		content, err = os.ReadFile(adminRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: ClusterRole"))

		content, err = os.ReadFile(editorRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: ClusterRole"))

		content, err = os.ReadFile(viewerRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: ClusterRole"))

		// ========== Part 5: Switch back to namespaced and verify again ==========
		By("switching back to namespace-scoped with --force")
		err = kbc.Edit("--namespaced", "--force")
		Expect(err).NotTo(HaveOccurred())

		By("verifying all roles reverted to namespace-scoped")
		content, err = os.ReadFile(roleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))

		content, err = os.ReadFile(adminRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))

		content, err = os.ReadFile(editorRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))

		content, err = os.ReadFile(viewerRoleFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("kind: Role"))

		By("verifying manager.yaml has WATCH_NAMESPACE again")
		content, err = os.ReadFile(managerFile)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(ContainSubstring("WATCH_NAMESPACE"))
	})
})


================================================
FILE: pkg/plugins/golang/v4/scaffolds/init.go
================================================
/*
Copyright 2022 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 scaffolds

import (
	"errors"
	"fmt"
	log "log/slog"
	"strings"

	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/github"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/test/utils"
)

const (
	// GolangciLintVersion is the golangci-lint version to be used in the project
	GolangciLintVersion = "v2.8.0"
	// ControllerRuntimeVersion is the kubernetes-sigs/controller-runtime version to be used in the project
	ControllerRuntimeVersion = "v0.23.3"
	// ControllerToolsVersion is the kubernetes-sigs/controller-tools version to be used in the project
	ControllerToolsVersion = "v0.20.1"

	imageName = "controller:latest"
)

var _ plugins.Scaffolder = &initScaffolder{}

var kustomizeVersion string

type initScaffolder struct {
	config          config.Config
	boilerplatePath string
	license         string
	owner           string
	commandName     string

	// fs is the filesystem that will be used by the scaffolder
	fs machinery.Filesystem
}

// NewInitScaffolder returns a new Scaffolder for project initialization operations
func NewInitScaffolder(cfg config.Config, license, owner, commandName string) plugins.Scaffolder {
	return &initScaffolder{
		config:          cfg,
		boilerplatePath: hack.DefaultBoilerplatePath,
		license:         license,
		owner:           owner,
		commandName:     commandName,
	}
}

// InjectFS implements cmdutil.Scaffolder
func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// getControllerRuntimeReleaseBranch converts the ControllerRuntime semantic versioning string to a
// release branch string. Example input: "v0.17.0" -> Output: "release-0.17"
func getControllerRuntimeReleaseBranch() string {
	v := strings.TrimPrefix(ControllerRuntimeVersion, "v")
	tmp := strings.Split(v, ".")

	if len(tmp) < 2 {
		fmt.Println("Invalid version format. Expected at least major and minor version numbers.")
		return ""
	}
	releaseBranch := fmt.Sprintf("release-%s.%s", tmp[0], tmp[1])
	return releaseBranch
}

// Scaffold implements cmdutil.Scaffolder
func (s *initScaffolder) Scaffold() error {
	log.Info("Writing scaffold for you to edit...")

	// Initialize the machinery.Scaffold that will write the boilerplate file to disk
	// The boilerplate file needs to be scaffolded as a separate step as it is going to
	// be used by the rest of the files, even those scaffolded in this command call.
	scaffold := machinery.NewScaffold(s.fs,
		machinery.WithConfig(s.config),
	)

	if s.license != "none" {
		bpFile := &hack.Boilerplate{
			License: s.license,
			Owner:   s.owner,
		}
		bpFile.Path = s.boilerplatePath
		if err := scaffold.Execute(bpFile); err != nil {
			return fmt.Errorf("failed to execute boilerplate: %w", err)
		}

		boilerplate, err := afero.ReadFile(s.fs.FS, s.boilerplatePath)
		if err != nil {
			if errors.Is(err, afero.ErrFileNotFound) {
				log.Warn("unable to find boilerplate file. "+
					"This file is used to generate the license header in the project.\n"+
					"Note that controller-gen will also use this. Ensure that you "+
					"add the license file or configure your project accordingly",
					"file_path", s.boilerplatePath,
					"error", err)
				boilerplate = []byte("")
			} else {
				return fmt.Errorf("failed to load boilerplate: %w", err)
			}
		}
		// Initialize the machinery.Scaffold that will write the files to disk
		scaffold = machinery.NewScaffold(s.fs,
			machinery.WithConfig(s.config),
			machinery.WithBoilerplate(string(boilerplate)),
		)
	} else {
		s.boilerplatePath = ""
		// Initialize the machinery.Scaffold without boilerplate
		scaffold = machinery.NewScaffold(s.fs,
			machinery.WithConfig(s.config),
		)
	}

	// If the KustomizeV2 was used to do the scaffold then
	// we need to ensure that we use its supported Kustomize Version
	// in order to support it
	kustomizev2 := kustomizecommonv2.Plugin{}
	gov4 := "go.kubebuilder.io/v4"
	pluginKeyForKustomizeV2 := plugin.KeyFor(kustomizev2)

	for _, pluginKey := range s.config.GetPluginChain() {
		if pluginKey == pluginKeyForKustomizeV2 || pluginKey == gov4 {
			kustomizeVersion = kustomizecommonv2.KustomizeVersion
			break
		}
	}

	err := scaffold.Execute(
		&cmd.Main{
			ControllerRuntimeVersion: ControllerRuntimeVersion,
		},
		&templates.GoMod{
			ControllerRuntimeVersion: ControllerRuntimeVersion,
		},
		&templates.GitIgnore{},
		&templates.Makefile{
			Image:                    imageName,
			BoilerplatePath:          s.boilerplatePath,
			ControllerToolsVersion:   ControllerToolsVersion,
			KustomizeVersion:         kustomizeVersion,
			GolangciLintVersion:      GolangciLintVersion,
			ControllerRuntimeVersion: ControllerRuntimeVersion,
			EnvtestVersion:           getControllerRuntimeReleaseBranch(),
		},
		&templates.Dockerfile{},
		&templates.DockerIgnore{},
		&templates.Readme{CommandName: s.commandName},
		&templates.Agents{CommandName: s.commandName},
		&templates.Golangci{},
		&templates.CustomGcl{
			GolangciLintVersion: GolangciLintVersion,
		},
		&e2e.Test{},
		&e2e.WebhookTestUpdater{WireWebhook: false},
		&e2e.SuiteTest{},
		&github.E2eTestCi{},
		&github.TestCi{},
		&github.LintCi{
			GolangciLintVersion: GolangciLintVersion,
		},
		&utils.Utils{},
		&templates.DevContainer{},
		&templates.DevContainerPostInstallScript{},
	)
	if err != nil {
		return fmt.Errorf("failed to execute init scaffold: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/init_integration_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

var _ = Describe("Init Scaffolding Integration Test", func() {
	var kbc *utils.TestContext

	BeforeEach(func() {
		var err error
		kbc, err = utils.NewTestContext(pluginutil.KubebuilderBinName, "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())
	})

	AfterEach(func() {
		kbc.Destroy()
	})

	Context("cluster-scoped init (default)", func() {
		It("should scaffold cluster-scoped configuration", func() {
			By("initializing a cluster-scoped project")
			err := kbc.Init(
				"--plugins", "go/v4",
				"--project-version", "3",
				"--domain", kbc.Domain,
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying PROJECT file does not have namespaced flag")
			projectFile := filepath.Join(kbc.Dir, "PROJECT")
			content, err := os.ReadFile(projectFile)
			Expect(err).NotTo(HaveOccurred())
			projectContent := string(content)
			// Check root level doesn't have namespaced: true
			beforeResources := strings.Split(projectContent, "resources:")[0]
			if strings.Contains(beforeResources, "namespaced:") {
				Expect(beforeResources).NotTo(ContainSubstring("namespaced: true"))
			}

			By("verifying RBAC is cluster-scoped")
			roleFile := filepath.Join(kbc.Dir, "config", "rbac", "role.yaml")
			content, err = os.ReadFile(roleFile)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(content)).To(ContainSubstring("kind: ClusterRole"))

			roleBindingFile := filepath.Join(kbc.Dir, "config", "rbac", "role_binding.yaml")
			content, err = os.ReadFile(roleBindingFile)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(content)).To(ContainSubstring("kind: ClusterRoleBinding"))

			By("verifying manager.yaml does NOT have WATCH_NAMESPACE")
			managerFile := filepath.Join(kbc.Dir, "config", "manager", "manager.yaml")
			content, err = os.ReadFile(managerFile)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(content)).NotTo(ContainSubstring("WATCH_NAMESPACE"))

			By("verifying cmd/main.go does NOT have namespace helper functions")
			mainFile := filepath.Join(kbc.Dir, "cmd", "main.go")
			content, err = os.ReadFile(mainFile)
			Expect(err).NotTo(HaveOccurred())
			mainContent := string(content)
			Expect(mainContent).NotTo(ContainSubstring("func getWatchNamespace()"))
			Expect(mainContent).NotTo(ContainSubstring("func setupCacheNamespaces"))
		})
	})

	Context("namespace-scoped init (--namespaced)", func() {
		It("should scaffold namespace-scoped configuration", func() {
			By("initializing a namespace-scoped project")
			err := kbc.Init(
				"--plugins", "go/v4",
				"--project-version", "3",
				"--domain", kbc.Domain,
				"--namespaced",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying PROJECT file has namespaced: true")
			projectFile := filepath.Join(kbc.Dir, "PROJECT")
			content, err := os.ReadFile(projectFile)
			Expect(err).NotTo(HaveOccurred())
			projectContent := string(content)
			// Check root level has namespaced: true
			beforeResources := strings.Split(projectContent, "resources:")[0]
			Expect(beforeResources).To(ContainSubstring("namespaced: true"))

			By("verifying RBAC is namespace-scoped")
			roleFile := filepath.Join(kbc.Dir, "config", "rbac", "role.yaml")
			content, err = os.ReadFile(roleFile)
			Expect(err).NotTo(HaveOccurred())
			roleContent := string(content)
			Expect(roleContent).To(ContainSubstring("kind: Role"))
			Expect(roleContent).NotTo(ContainSubstring("kind: ClusterRole"))

			roleBindingFile := filepath.Join(kbc.Dir, "config", "rbac", "role_binding.yaml")
			content, err = os.ReadFile(roleBindingFile)
			Expect(err).NotTo(HaveOccurred())
			bindingContent := string(content)
			Expect(bindingContent).To(ContainSubstring("kind: RoleBinding"))
			Expect(bindingContent).NotTo(ContainSubstring("kind: ClusterRoleBinding"))

			By("verifying manager.yaml has WATCH_NAMESPACE environment variable")
			managerFile := filepath.Join(kbc.Dir, "config", "manager", "manager.yaml")
			content, err = os.ReadFile(managerFile)
			Expect(err).NotTo(HaveOccurred())
			managerContent := string(content)
			Expect(managerContent).To(ContainSubstring("- name: WATCH_NAMESPACE"))
			Expect(managerContent).To(ContainSubstring("fieldRef:"))
			Expect(managerContent).To(ContainSubstring("fieldPath: metadata.namespace"))

			By("verifying cmd/main.go has getWatchNamespace helper function")
			mainFile := filepath.Join(kbc.Dir, "cmd", "main.go")
			content, err = os.ReadFile(mainFile)
			Expect(err).NotTo(HaveOccurred())
			mainContent := string(content)
			Expect(mainContent).To(ContainSubstring("func getWatchNamespace()"))
			Expect(mainContent).To(ContainSubstring("WATCH_NAMESPACE"))

			By("verifying cmd/main.go has setupCacheNamespaces helper function")
			Expect(mainContent).To(ContainSubstring("func setupCacheNamespaces"))
			Expect(mainContent).To(ContainSubstring("DefaultNamespaces"))
			Expect(mainContent).To(ContainSubstring("cache.Config"))

			By("verifying cmd/main.go uses helper functions in main()")
			Expect(mainContent).To(ContainSubstring("watchNamespace, err := getWatchNamespace()"))
			Expect(mainContent).To(ContainSubstring("mgrOptions.Cache = setupCacheNamespaces(watchNamespace)"))

			By("creating an API to verify controller scaffolding")
			err = kbc.CreateAPI(
				"--group", kbc.Group,
				"--version", kbc.Version,
				"--kind", kbc.Kind,
				"--resource",
				"--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying controller has namespace parameter in RBAC markers")
			controllerFile := filepath.Join(kbc.Dir, "internal", "controller",
				strings.ToLower(kbc.Kind)+"_controller.go")
			content, err = os.ReadFile(controllerFile)
			Expect(err).NotTo(HaveOccurred())
			controllerContent := string(content)

			expectedNamespace := "e2e-" + kbc.TestSuffix + "-system"
			Expect(controllerContent).To(ContainSubstring("namespace="+expectedNamespace),
				"Controller RBAC markers should include namespace parameter")

			By("verifying admin/editor/viewer roles are namespace-scoped")
			editorFile := filepath.Join(kbc.Dir, "config", "rbac",
				strings.ToLower(kbc.Kind)+"_editor_role.yaml")
			content, err = os.ReadFile(editorFile)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(content)).To(ContainSubstring("kind: Role"))
			Expect(string(content)).NotTo(ContainSubstring("kind: ClusterRole"))
		})
	})
})


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/agents.go
================================================
/*
Copyright 2022 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 templates

import (
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Agents{}

// Agents scaffolds an AGENTS.md file
type Agents struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// CommandName stores the name of the bin used
	CommandName string
	// IsKubebuilderCLI indicates if kubebuilder CLI is being used (vs operator-sdk, etc)
	IsKubebuilderCLI bool
}

// SetTemplateDefaults implements machinery.Template
func (f *Agents) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = "AGENTS.md"
	}

	// Check if using Kubebuilder CLI
	if f.CommandName != "" {
		f.IsKubebuilderCLI = strings.Contains(f.CommandName, "kubebuilder")
	}

	f.TemplateBody = agentsFileTemplate

	return nil
}

//nolint:lll
const agentsFileTemplate = `# {{ .ProjectName }} - AI Agent Guide

## Project Structure

**Single-group layout (default):**
` + "```" + `
cmd/main.go                    Manager entry (registers controllers/webhooks)
api//*_types.go       CRD schemas (+kubebuilder markers)
api//zz_generated.*   Auto-generated (DO NOT EDIT)
internal/controller/*          Reconciliation logic
internal/webhook/*             Validation/defaulting (if present)
config/crd/bases/*             Generated CRDs (DO NOT EDIT)
config/rbac/role.yaml          Generated RBAC (DO NOT EDIT)
config/samples/*               Example CRs (edit these)
Makefile                       Build/test/deploy commands
PROJECT                        Kubebuilder metadata Auto-generated (DO NOT EDIT)
` + "```" + `

**Multi-group layout** (for projects with multiple API groups):
` + "```" + `
api///*_types.go       CRD schemas by group
internal/controller//*          Controllers by group
internal/webhook///*   Webhooks by group and version (if present)
` + "```" + `

Multi-group layout organizes APIs by group name (e.g., ` + "`batch`" + `, ` + "`apps`" + `). Check the ` + "`PROJECT`" + ` file for ` + "`multigroup: true`" + `.

**To convert to multi-group layout:**
1. Run: ` + "`{{ .CommandName }} edit --multigroup=true`" + `
2. Move APIs: ` + "`mkdir -p api/ && mv api/ api//`" + `
3. Move controllers: ` + "`mkdir -p internal/controller/ && mv internal/controller/*.go internal/controller//`" + `
4. Move webhooks (if present): ` + "`mkdir -p internal/webhook/ && mv internal/webhook/ internal/webhook//`" + `
5. Update import paths in all files
6. Fix ` + "`path`" + ` in ` + "`PROJECT`" + ` file for each resource
7. Update test suite CRD paths (add one more ` + "`..`" + ` to relative paths)

## Critical Rules

### Never Edit These (Auto-Generated)
- ` + "`config/crd/bases/*.yaml`" + ` - from ` + "`make manifests`" + `
- ` + "`config/rbac/role.yaml`" + ` - from ` + "`make manifests`" + `
- ` + "`config/webhook/manifests.yaml`" + ` - from ` + "`make manifests`" + `
- ` + "`**/zz_generated.*.go`" + ` - from ` + "`make generate`" + `
- ` + "`PROJECT`" + ` - from ` + "`{{ .CommandName }} [OPTIONS]`" + `

### Never Remove Scaffold Markers
Do NOT delete ` + "`// +kubebuilder:scaffold:*`" + ` comments. CLI injects code at these markers.

### Keep Project Structure
Do not move files around. The CLI expects files in specific locations.

### Always Use CLI Commands
Always use ` + "`{{ .CommandName }} create api`" + ` and ` + "`{{ .CommandName }} create webhook`" + ` to scaffold. Do NOT create files manually.

### E2E Tests Require an Isolated Kind Cluster
The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI).
Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster).

## After Making Changes

**After editing ` + "`*_types.go`" + ` or markers:**
` + "```" + `
make manifests  # Regenerate CRDs/RBAC from markers
make generate   # Regenerate DeepCopy methods
` + "```" + `

**After editing ` + "`*.go`" + ` files:**
` + "```" + `
make lint-fix   # Auto-fix code style
make test       # Run unit tests
` + "```" + `

## CLI Commands Cheat Sheet

### Create API (your own types)
` + "```bash" + `
{{ .CommandName }} create api --group  --version  --kind 
` + "```" + `{{ if .IsKubebuilderCLI }}

### Deploy Image Plugin (scaffold to deploy/manage ANY container image)

Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.):

` + "```bash" + `
# Example: deploying memcached
{{ .CommandName }} create api --group example.com --version v1alpha1 --kind Memcached \
  --image=memcached:alpine \
  --plugins=deploy-image.go.kubebuilder.io/v1-alpha
` + "```" + `

Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation.
{{ end }}

### Create Webhooks
` + "```bash" + `
# Validation + defaulting
{{ .CommandName }} create webhook --group  --version  --kind  \
  --defaulting --programmatic-validation

# Conversion webhook (for multi-version APIs)
{{ .CommandName }} create webhook --group  --version v1 --kind  \
  --conversion --spoke v2
` + "```" + `

### Controller for Core Kubernetes Types
` + "```bash" + `
# Watch Pods
{{ .CommandName }} create api --group core --version v1 --kind Pod \
  --controller=true --resource=false

# Watch Deployments
{{ .CommandName }} create api --group apps --version v1 --kind Deployment \
  --controller=true --resource=false
` + "```" + `

### Controller for External Types (e.g., from other operators)

Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.):

` + "```bash" + `
# Example: watching cert-manager Certificate resources
{{ .CommandName }} create api \
  --group cert-manager --version v1 --kind Certificate \
  --controller=true --resource=false \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
` + "```" + `

**Note:** Use ` + "`--external-api-module=@`" + ` only if you need a specific version. Otherwise, omit ` + "`@`" + ` to use what's in go.mod.

### Webhook for External Types

` + "```bash" + `
# Example: validating external resources
{{ .CommandName }} create webhook \
  --group cert-manager --version v1 --kind Issuer \
  --defaulting \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
` + "```" + `

## Testing & Development

` + "```bash" + `
make test              # Run unit tests (uses envtest: real K8s API + etcd)
make run               # Run locally (uses current kubeconfig context)
` + "```" + `

Tests use **Ginkgo + Gomega** (BDD style). Check ` + "`suite_test.go`" + ` for setup.

## Deployment Workflow

` + "```bash" + `
# 1. Regenerate manifests
make manifests generate

# 2. Build & deploy
export IMG=/:tag
make docker-build docker-push IMG=$IMG  # Or: kind load docker-image $IMG --name 
make deploy IMG=$IMG

# 3. Test
kubectl apply -k config/samples/

# 4. Debug
kubectl logs -n -system deployment/-controller-manager -c manager -f
` + "```" + `

### API Design

**Key markers for** ` + "`api//*_types.go`" + `:

` + "```go" + `
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"

// On fields:
// +kubebuilder:validation:Required
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:MaxLength=100
// +kubebuilder:validation:Pattern="^[a-z]+$"
// +kubebuilder:default="value"
` + "```" + `

- **Use** ` + "`metav1.Condition`" + ` for status (not custom string fields)
- **Use predefined types**: ` + "`metav1.Time`" + ` instead of ` + "`string`" + ` for dates
- **Follow K8s API conventions**: Standard field names (` + "`spec`" + `, ` + "`status`" + `, ` + "`metadata`" + `)

### Controller Design

**RBAC markers in** ` + "`internal/controller/*_controller.go`" + `:

` + "```go" + `
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
` + "```" + `

**Implementation rules:**
- **Idempotent reconciliation**: Safe to run multiple times
- **Re-fetch before updates**: ` + "`r.Get(ctx, req.NamespacedName, obj)`" + ` before ` + "`r.Update`" + ` to avoid conflicts
- **Structured logging**: ` + "`log := log.FromContext(ctx); log.Info(\"msg\", \"key\", val)`" + `
- **Owner references**: Enable automatic garbage collection (` + "`SetControllerReference`" + `)
- **Watch secondary resources**: Use ` + "`.Owns()`" + ` or ` + "`.Watches()`" + `, not just ` + "`RequeueAfter`" + `
- **Finalizers**: Clean up external resources (buckets, VMs, DNS entries)

### Logging

**Follow Kubernetes logging message style guidelines:**

- Start from a capital letter
- Do not end the message with a period
- Active voice: subject present (` + "`\"Deployment could not create Pod\"`" + `) or omitted (` + "`\"Could not create Pod\"`" + `)
- Past tense: ` + "`\"Could not delete Pod\"`" + ` not ` + "`\"Cannot delete Pod\"`" + `
- Specify object type: ` + "`\"Deleted Pod\"`" + ` not ` + "`\"Deleted\"`" + `
- Balanced key-value pairs

` + "```go" + `
log.Info("Starting reconciliation")
log.Info("Created Deployment", "name", deploy.Name)
log.Error(err, "Failed to create Pod", "name", name)
` + "```" + `

**Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines

### Webhooks
- **Create all types together**: ` + "`--defaulting --programmatic-validation --conversion`" + `
- **When` + "`--force`" + `is used**: Backup custom logic first, then restore after scaffolding
- **For multi-version APIs**: Use hub-and-spoke pattern (` + "`--conversion --spoke v2`" + `)
  - Hub version: Usually oldest stable version (v1)
  - Spoke versions: Newer versions that convert to/from hub (v2, v3)
  - Example: ` + "`--group crew --version v1 --kind Captain --conversion --spoke v2`" + ` (v1 is hub, v2 is spoke){{ if .IsKubebuilderCLI }}

### Learning from Examples

The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation:

` + "```bash" + `
{{ .CommandName }} create api --group example --version v1alpha1 --kind MyApp \
  --image= --plugins=deploy-image.go.kubebuilder.io/v1-alpha
` + "```" + `

Generated code includes: status conditions (` + "`metav1.Condition`" + `), finalizers, owner references, events, idempotent reconciliation.

## Distribution Options

### Option 1: YAML Bundle (Kustomize)

` + "```bash" + `
# Generate dist/install.yaml from Kustomize manifests
make build-installer IMG=/:tag
` + "```" + `

**Key points:**
- The ` + "`dist/install.yaml`" + ` is generated from Kustomize manifests (CRDs, RBAC, Deployment)
- Commit this file to your repository for easy distribution
- Users only need ` + "`kubectl`" + ` to install (no additional tools required)

**Example:** Users install with a single command:
` + "```bash" + `
kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml
` + "```" + `

### Option 2: Helm Chart

` + "```bash" + `
{{ .CommandName }} edit --plugins=helm/v2-alpha                      # Generates dist/chart/ (default)
{{ .CommandName }} edit --plugins=helm/v2-alpha --output-dir=charts  # Generates charts/chart/
` + "```" + `

**For development:**
` + "```bash" + `
make helm-deploy IMG=/:          # Deploy manager via Helm
make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..."    # Deploy with custom values
make helm-status                                         # Show release status
make helm-uninstall                                      # Remove release
make helm-history                                        # View release history
make helm-rollback                                       # Rollback to previous version
` + "```" + `

**For end users/production:**
` + "```bash" + `
helm install my-release .//chart/ --namespace  --create-namespace
` + "```" + `

**Important:** If you add webhooks or modify manifests after initial chart generation:
1. Backup any customizations in ` + "`/chart/values.yaml`" + ` and ` + "`/chart/manager/manager.yaml`" + `
2. Re-run: ` + "`{{ .CommandName }} edit --plugins=helm/v2-alpha --force`" + ` (use same ` + "`--output-dir`" + ` if customized)
3. Manually restore your custom values from the backup

### Publish Container Image

` + "```bash" + `
export IMG=/:
make docker-build docker-push IMG=$IMG
` + "```" + `{{ end }}

## References

### Essential Reading
- **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide)
- **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions)
- **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.)
- **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels)

### API Design & Implementation
- **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- **Markers Reference**: https://book.kubebuilder.io/reference/markers.html

### Tools & Libraries
- **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime
- **controller-tools**: https://github.com/kubernetes-sigs/controller-tools
- **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go
================================================
/*
Copyright 2022 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 api

import (
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Group{}

// Group scaffolds the file that defines the registration methods for a certain group and version
type Group struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *Group) SetTemplateDefaults() error {
	if f.Path == "" {
		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join("api", "%[group]", "%[version]", "groupversion_info.go")
		} else {
			f.Path = filepath.Join("api", "%[version]", "groupversion_info.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)
	f.TemplateBody = groupTemplate

	return nil
}

//nolint:lll
const groupTemplate = `{{ .Boilerplate }}

// Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group.
// +kubebuilder:object:generate=true
// +groupName={{ .Resource.QualifiedGroup }}
package {{ .Resource.Version }}

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "{{ .Resource.QualifiedGroup }}", Version: "{{ .Resource.Version }}"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/api/hub.go
================================================
/*
Copyright 2022 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 api

import (
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Hub{}

// Hub scaffolds the file that defines hub
//

type Hub struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	Force bool
}

// SetTemplateDefaults implements file.Template
func (f *Hub) SetTemplateDefaults() error {
	if f.Path == "" {
		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_conversion.go")
		} else {
			f.Path = filepath.Join("api", "%[version]", "%[kind]_conversion.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	f.TemplateBody = hubTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

const hubTemplate = `{{ .Boilerplate }}

package {{ .Resource.Version }}

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// Hub marks this type as a conversion hub.
func (*{{ .Resource.Kind }}) Hub() {}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/api/spoke.go
================================================
/*
Copyright 2022 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 api

import (
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Spoke{}

// Spoke scaffolds the file that defines spoke version conversion
type Spoke struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	Force        bool
	SpokeVersion string
}

// SetTemplateDefaults implements file.Template
func (f *Spoke) SetTemplateDefaults() error {
	if f.Path == "" {
		if f.MultiGroup && f.Resource.Group != "" {
			// Use SpokeVersion for dynamic file path generation
			f.Path = filepath.Join("api", f.Resource.Group, f.SpokeVersion, "%[kind]_conversion.go")
		} else {
			f.Path = filepath.Join("api", f.SpokeVersion, "%[kind]_conversion.go")
		}
	}

	// Replace placeholders in the path
	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info("Creating spoke conversion file", "path", f.Path)

	f.TemplateBody = spokeTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

//nolint:lll
const spokeTemplate = `{{ .Boilerplate }}

package {{ .SpokeVersion }}

import (
	"log"

    "sigs.k8s.io/controller-runtime/pkg/conversion"
    {{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
)

// ConvertTo converts this {{ .Resource.Kind }} ({{ .SpokeVersion }}) to the Hub version ({{ .Resource.Version }}).
func (src *{{ .Resource.Kind }}) ConvertTo(dstRaw conversion.Hub) error {
	dst := dstRaw.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
	log.Printf("ConvertTo: Converting {{ .Resource.Kind }} from Spoke version {{ .SpokeVersion }} to Hub version {{ .Resource.Version }};" +
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)
	
	// TODO(user): Implement conversion logic from {{ .SpokeVersion }} to {{ .Resource.Version }}
	// Example: Copying Spec fields
	// dst.Spec.Size = src.Spec.Replicas

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}

// ConvertFrom converts the Hub version ({{ .Resource.Version }}) to this {{ .Resource.Kind }} ({{ .SpokeVersion }}).
func (dst *{{ .Resource.Kind }}) ConvertFrom(srcRaw conversion.Hub) error {
	src := srcRaw.(*{{ .Resource.ImportAlias }}.{{ .Resource.Kind }})
	log.Printf("ConvertFrom: Converting {{ .Resource.Kind }} from Hub version {{ .Resource.Version }} to Spoke version {{ .SpokeVersion }};" +
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)

	// TODO(user): Implement conversion logic from {{ .Resource.Version }} to {{ .SpokeVersion }}
	// Example: Copying Spec fields
	// dst.Spec.Replicas = src.Spec.Size

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go
================================================
/*
Copyright 2022 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 api

import (
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Types{}

// Types scaffolds the file that defines the schema for a CRD
//

type Types struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	Force bool
}

// SetTemplateDefaults implements machinery.Template
func (f *Types) SetTemplateDefaults() error {
	if f.Path == "" {
		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_types.go")
		} else {
			f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	f.TemplateBody = typesTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.Error
	}

	return nil
}

//nolint:lll
const typesTemplate = `{{ .Boilerplate }}

package {{ .Resource.Version }}

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// {{ .Resource.Kind }}Spec defines the desired state of {{ .Resource.Kind }}
type {{ .Resource.Kind }}Spec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of {{ .Resource.Kind }}. Edit {{ lower .Resource.Kind }}_types.go to remove/update
	// +optional	
	Foo *string ` + "`" + `json:"foo,omitempty"` + "`" + `
}

// {{ .Resource.Kind }}Status defines the observed state of {{ .Resource.Kind }}.
type {{ .Resource.Kind }}Status struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the {{ .Resource.Kind }} resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition ` + "`" + `json:"conditions,omitempty"` + "`" + `
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
{{- if and (not .Resource.API.Namespaced) (not .Resource.IsRegularPlural) }}
// +kubebuilder:resource:path={{ .Resource.Plural }},scope=Cluster
{{- else if not .Resource.API.Namespaced }}
// +kubebuilder:resource:scope=Cluster
{{- else if not .Resource.IsRegularPlural }}
// +kubebuilder:resource:path={{ .Resource.Plural }}
{{- end }}

// {{ .Resource.Kind }} is the Schema for the {{ .Resource.Plural }} API
type {{ .Resource.Kind }} struct {
	metav1.TypeMeta   ` + "`" + `json:",inline"` + "`" + `

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta ` + "`" + `json:"metadata,omitzero"` + "`" + `

	// spec defines the desired state of {{ .Resource.Kind }}
	// +required
	Spec   {{ .Resource.Kind }}Spec   ` + "`" + `json:"spec"` + "`" + `

	// status defines the observed state of {{ .Resource.Kind }}
	// +optional
	Status {{ .Resource.Kind }}Status ` + "`" + `json:"status,omitzero"` + "`" + `
}

// +kubebuilder:object:root=true

// {{ .Resource.Kind }}List contains a list of {{ .Resource.Kind }}
type {{ .Resource.Kind }}List struct {
	metav1.TypeMeta ` + "`" + `json:",inline"` + "`" + `
	metav1.ListMeta ` + "`" + `json:"metadata,omitzero"` + "`" + `
	Items           []{{ .Resource.Kind }} ` + "`" + `json:"items"` + "`" + `
}

func init() {
	SchemeBuilder.Register(&{{ .Resource.Kind }}{}, &{{ .Resource.Kind }}List{})
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/api/types_updater.go
================================================
/*
Copyright 2026 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 api

import (
	"bytes"
	"fmt"
	log "log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

const (
	storageVersionMarker = "\n// +kubebuilder:storageversion"
)

var _ machinery.Template = &TypesUpdater{}

// TypesUpdater updates an existing API types file to add conversion-related markers
type TypesUpdater struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.ResourceMixin
}

// GetPath implements file.Builder
func (f *TypesUpdater) GetPath() string {
	if f.MultiGroup && f.Resource.Group != "" {
		f.Path = filepath.Join("api", "%[group]", "%[version]", "%[kind]_types.go")
	} else {
		f.Path = filepath.Join("api", "%[version]", "%[kind]_types.go")
	}

	return f.Resource.Replacer().Replace(f.Path)
}

// GetIfExistsAction implements file.Builder
func (*TypesUpdater) GetIfExistsAction() machinery.IfExistsAction {
	return machinery.OverwriteFile
}

// SetTemplateDefaults implements file.Template
func (f *TypesUpdater) SetTemplateDefaults() error {
	filePath := f.GetPath()

	// Read the existing file
	content, err := os.ReadFile(filePath)
	if err != nil {
		log.Error("failed to read types file", "file", filePath, "error", err)
		return fmt.Errorf("failed to read types file: %w", err)
	}

	fileContent := string(content)
	modified := false

	// Check if we need to add storage version marker for conversion webhooks
	if f.Resource.HasConversionWebhook() && !bytes.Contains(content, []byte("+kubebuilder:storageversion")) {
		fileContent = f.addStorageVersionMarker(fileContent)
		modified = true
	}

	if !modified {
		// No updates needed, skip writing
		return nil
	}

	f.TemplateBody = fileContent
	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

// addStorageVersionMarker adds the storage version marker after +kubebuilder:object:root=true
func (f *TypesUpdater) addStorageVersionMarker(content string) string {
	// Try to match the specific Kind's type definition (handles multigroup with multiple types)
	typePatternStr := fmt.Sprintf(
		`(?m)^(//\s*\+kubebuilder:object:root=true)\s*$(?:\s*//.*$)*\s*type\s+%s\s+struct`,
		f.Resource.Kind)
	typePattern := regexp.MustCompile(typePatternStr)

	if match := typePattern.FindStringSubmatch(content); len(match) > 1 {
		rootMarker := match[1]
		idx := strings.Index(content, rootMarker)
		if idx != -1 {
			insertPos := idx + len(rootMarker)
			return content[:insertPos] + storageVersionMarker + content[insertPos:]
		}
	}

	// Fallback: find first +kubebuilder:object:root=true marker
	simplePattern := regexp.MustCompile(`(?m)^(//\s*\+kubebuilder:object:root=true)\s*$`)
	if match := simplePattern.FindStringIndex(content); match != nil {
		log.Info("Adding storage version marker to first type definition",
			"kind", f.Resource.Kind)
		return content[:match[1]] + storageVersionMarker + content[match[1]:]
	}

	log.Warn("Could not find +kubebuilder:object:root=true marker",
		"kind", f.Resource.Kind,
		"suggestion", "Manually add // +kubebuilder:storageversion")

	return content
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/cmd/main.go
================================================
/*
Copyright 2022 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 cmd

import (
	"fmt"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

const defaultMainPath = "cmd/main.go"

var _ machinery.Template = &Main{}

// Main scaffolds a file that defines the controller manager entry point
type Main struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin
	machinery.DomainMixin
	machinery.RepositoryMixin
	machinery.NamespacedMixin

	ControllerRuntimeVersion string
}

// SetTemplateDefaults implements machinery.Template
func (f *Main) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join(defaultMainPath)
	}

	f.TemplateBody = fmt.Sprintf(mainTemplate,
		machinery.NewMarkerFor(f.Path, importMarker),
		machinery.NewMarkerFor(f.Path, addSchemeMarker),
		machinery.NewMarkerFor(f.Path, setupMarker),
	)

	return nil
}

var _ machinery.Inserter = &MainUpdater{}

// MainUpdater updates cmd/main.go to run Controllers
type MainUpdater struct {
	machinery.RepositoryMixin
	machinery.MultiGroupMixin
	machinery.ResourceMixin

	// Flags to indicate which parts need to be included when updating the file
	WireResource, WireController, WireWebhook bool

	// Deprecated - The flag should be removed from go/v5
	// IsLegacyPath indicates if webhooks should be scaffolded under the API.
	// Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
	// This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
	IsLegacyPath bool
}

// GetPath implements file.Builder
func (*MainUpdater) GetPath() string {
	return defaultMainPath
}

// GetIfExistsAction implements file.Builder
func (*MainUpdater) GetIfExistsAction() machinery.IfExistsAction {
	return machinery.OverwriteFile
}

const (
	importMarker    = "imports"
	addSchemeMarker = "scheme"
	setupMarker     = "builder"
)

// GetMarkers implements file.Inserter
func (f *MainUpdater) GetMarkers() []machinery.Marker {
	return []machinery.Marker{
		machinery.NewMarkerFor(defaultMainPath, importMarker),
		machinery.NewMarkerFor(defaultMainPath, addSchemeMarker),
		machinery.NewMarkerFor(defaultMainPath, setupMarker),
	}
}

const (
	apiImportCodeFragment = `%s "%s"
`
	controllerImportCodeFragment = `"%s/internal/controller"
`
	webhookImportCodeFragment = `%s "%s/internal/webhook/%s"
`
	multiGroupWebhookImportCodeFragment = `%s "%s/internal/webhook/%s/%s"
`
	multiGroupControllerImportCodeFragment = `%scontroller "%s/internal/controller/%s"
`
	addschemeCodeFragment = `utilruntime.Must(%s.AddToScheme(scheme))
`
	reconcilerSetupCodeFragment = `if err := (&controller.%sReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "%s")
		os.Exit(1)
	}
`
	multiGroupReconcilerSetupCodeFragment = `if err := (&%scontroller.%sReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "%s")
		os.Exit(1)
	}
`
	webhookSetupCodeFragmentLegacy = `// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := (&%s.%s{}).SetupWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "%s")
			os.Exit(1)
		}
	}
`

	webhookSetupCodeFragment = `// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := %s.Setup%sWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "%s")
			os.Exit(1)
		}
	}
`
)

// GetCodeFragments implements file.Inserter
func (f *MainUpdater) GetCodeFragments() machinery.CodeFragmentsMap {
	fragments := make(machinery.CodeFragmentsMap, 3)

	// If resource is not being provided we are creating the file, not updating it
	if f.Resource == nil {
		return fragments
	}

	// Generate import code fragments
	imports := make([]string, 0)
	if f.WireResource || f.Resource.IsExternal() {
		imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
	}
	if f.WireWebhook && !f.IsLegacyPath {
		if !f.MultiGroup || f.Resource.Group == "" {
			importPath := fmt.Sprintf("webhook%s", f.Resource.Version)
			imports = append(imports, fmt.Sprintf(webhookImportCodeFragment, importPath, f.Repo, f.Resource.Version))
		} else {
			importPath := fmt.Sprintf("webhook%s", f.Resource.ImportAlias())
			imports = append(imports, fmt.Sprintf(multiGroupWebhookImportCodeFragment, importPath,
				f.Repo, f.Resource.Group, f.Resource.Version))
		}
	}

	if f.WireController {
		if !f.MultiGroup || f.Resource.Group == "" {
			imports = append(imports, fmt.Sprintf(controllerImportCodeFragment, f.Repo))
		} else {
			imports = append(imports, fmt.Sprintf(multiGroupControllerImportCodeFragment,
				f.Resource.PackageName(), f.Repo, f.Resource.Group))
		}
	}

	// Generate add scheme code fragments
	addScheme := make([]string, 0)
	if f.WireResource || f.Resource.IsExternal() {
		addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
	}

	// Generate setup code fragments
	setup := make([]string, 0)
	if f.WireController {
		if !f.MultiGroup || f.Resource.Group == "" {
			setup = append(setup, fmt.Sprintf(reconcilerSetupCodeFragment,
				f.Resource.Kind, f.Resource.Kind))
		} else {
			setup = append(setup, fmt.Sprintf(multiGroupReconcilerSetupCodeFragment,
				f.Resource.PackageName(), f.Resource.Kind, f.Resource.Kind))
		}
	}
	if f.WireWebhook {
		if f.IsLegacyPath {
			setup = append(setup, fmt.Sprintf(webhookSetupCodeFragmentLegacy,
				f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind))
		} else {
			if !f.MultiGroup || f.Resource.Group == "" {
				setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment,
					"webhook"+f.Resource.Version, f.Resource.Kind, f.Resource.Kind))
			} else {
				setup = append(setup, fmt.Sprintf(webhookSetupCodeFragment,
					"webhook"+f.Resource.ImportAlias(), f.Resource.Kind, f.Resource.Kind))
			}
		}
	}

	// Only store code fragments in the map if the slices are non-empty
	if len(imports) != 0 {
		fragments[machinery.NewMarkerFor(defaultMainPath, importMarker)] = imports
	}
	if len(addScheme) != 0 {
		fragments[machinery.NewMarkerFor(defaultMainPath, addSchemeMarker)] = addScheme
	}
	if len(setup) != 0 {
		fragments[machinery.NewMarkerFor(defaultMainPath, setupMarker)] = setup
	}

	return fragments
}

//nolint:lll
var mainTemplate = `{{ .Boilerplate }}

package main

import (
	"crypto/tls"
	"flag"
{{- if .Namespaced }}
	"fmt"
{{- end }}
	"os"
{{- if .Namespaced }}
	"strings"
{{- end }}

	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
	// to ensure that exec-entrypoint and run can make use of them.
	_ "k8s.io/client-go/plugin/pkg/client/auth"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
{{- if .Namespaced }}
	"sigs.k8s.io/controller-runtime/pkg/cache"
{{- end }}
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	%s
)

var (
	scheme = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))

	%s
}
{{- if .Namespaced }}

// getWatchNamespace returns the namespace(s) the manager should watch for changes.
// It reads the value from the WATCH_NAMESPACE environment variable.
// - If WATCH_NAMESPACE is not set, an error is returned
// - If WATCH_NAMESPACE contains a single namespace, the manager watches that namespace
// - If WATCH_NAMESPACE contains comma-separated namespaces, the manager watches those namespaces
func getWatchNamespace() (string, error) {
	watchNamespaceEnvVar := "WATCH_NAMESPACE"
	ns, found := os.LookupEnv(watchNamespaceEnvVar)
	if !found {
		return "", fmt.Errorf("%%s must be set", watchNamespaceEnvVar)
	}
	return ns, nil
}

// setupCacheNamespaces configures the cache to watch specific namespace(s).
// It supports both single namespace ("ns1") and multi-namespace ("ns1,ns2,ns3") formats.
func setupCacheNamespaces(namespaces string) cache.Options {
	defaultNamespaces := make(map[string]cache.Config)
	for ns := range strings.SplitSeq(namespaces, ",") {
		defaultNamespaces[strings.TrimSpace(ns)] = cache.Config{}
	}
	return cache.Options{
		DefaultNamespaces: defaultNamespaces,
	}
}
{{- end }}

// nolint:gocyclo
func main() {
	var metricsAddr string
	var metricsCertPath, metricsCertName, metricsCertKey string
	var webhookCertPath, webhookCertName, webhookCertKey string
	var enableLeaderElection bool
	var probeAddr string
	var secureMetrics bool
	var enableHTTP2 bool
	var tlsOpts []func(*tls.Config)
	flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. " +
		"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
		"Enable leader election for controller manager. " +
		"Enabling this will ensure there is only one active controller manager.")
	flag.BoolVar(&secureMetrics, "metrics-secure", true,
		"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
	flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
	flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
	flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
	flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
		"The directory that contains the metrics server certificate.")
	flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
	flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
	flag.BoolVar(&enableHTTP2, "enable-http2", false,
		"If set, HTTP/2 will be enabled for the metrics and webhook servers")
	opts := zap.Options{
		Development: true,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	// if the enable-http2 flag is false (the default), http/2 should be disabled
	// due to its vulnerabilities. More specifically, disabling http/2 will
	// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
	// Rapid Reset CVEs. For more information see:
	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
	// - https://github.com/advisories/GHSA-4374-p667-p6c8
	disableHTTP2 := func(c *tls.Config) {
		setupLog.Info("Disabling HTTP/2")
		c.NextProtos = []string{"http/1.1"}
	}

	if !enableHTTP2 {
		tlsOpts = append(tlsOpts, disableHTTP2)
	}

	// Initial webhook TLS options
	webhookTLSOpts := tlsOpts
	webhookServerOptions := webhook.Options{
		TLSOpts: webhookTLSOpts,
	}

	if len(webhookCertPath) > 0 {
		setupLog.Info("Initializing webhook certificate watcher using provided certificates",
			"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)

		webhookServerOptions.CertDir = webhookCertPath
		webhookServerOptions.CertName = webhookCertName
		webhookServerOptions.KeyName = webhookCertKey
	}

	webhookServer := webhook.NewServer(webhookServerOptions)

	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
	// More info:
	// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/metrics/server
	// - https://book.kubebuilder.io/reference/metrics.html
	metricsServerOptions := metricsserver.Options{
		BindAddress:   metricsAddr,
		SecureServing: secureMetrics,
		TLSOpts: tlsOpts,
	}

	if secureMetrics {
		// FilterProvider is used to protect the metrics endpoint with authn/authz.
		// These configurations ensure that only authorized users and service accounts
		// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
		// https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/metrics/filters#WithAuthenticationAndAuthorization
		metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
	}

	// If the certificate is not specified, controller-runtime will automatically
	// generate self-signed certificates for the metrics server. While convenient for development and testing,
	// this setup is not recommended for production.
	//
	// TODO(user): If you enable certManager, uncomment the following lines:
	// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
	// managed by cert-manager for the metrics server.
	// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
	if len(metricsCertPath) > 0 {
		setupLog.Info("Initializing metrics certificate watcher using provided certificates",
			"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)

		metricsServerOptions.CertDir = metricsCertPath
		metricsServerOptions.CertName = metricsCertName
		metricsServerOptions.KeyName = metricsCertKey
	}
{{- if .Namespaced }}

	// Get the namespace(s) for namespace-scoped mode from WATCH_NAMESPACE environment variable.
	// The manager will only watch and manage resources in the specified namespace(s).
	watchNamespace, err := getWatchNamespace()
	if err != nil {
		setupLog.Error(err, "Unable to get WATCH_NAMESPACE, "+
			"the manager will watch and manage resources in all namespaces")
		os.Exit(1)
	}

	// Configure manager options for namespace-scoped mode
	mgrOptions := ctrl.Options{
		Scheme:                 scheme,
		Metrics:                metricsServerOptions,
		WebhookServer:          webhookServer,
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		{{- if not .Domain }}
		LeaderElectionID:       "{{ hashFNV .Repo }}",
		{{- else }}
		LeaderElectionID:       "{{ hashFNV .Repo }}.{{ .Domain }}",
		{{- end }}
		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	}

	// Configure cache to watch namespace(s) specified in WATCH_NAMESPACE
	mgrOptions.Cache = setupCacheNamespaces(watchNamespace)
	setupLog.Info("Watching namespace(s)", "namespaces", watchNamespace)

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions)
{{- else }}

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme: scheme,
		Metrics:                metricsServerOptions,
		WebhookServer:          webhookServer,
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		{{- if not .Domain }}
		LeaderElectionID:        "{{ hashFNV .Repo }}",
		{{- else }}
		LeaderElectionID:        "{{ hashFNV .Repo }}.{{ .Domain }}",
		{{- end }}
		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	})
{{- end }}
	if err != nil {
		setupLog.Error(err, "Failed to start manager")
		os.Exit(1)
	}

	%s

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up health check")
		os.Exit(1)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up ready check")
		os.Exit(1)
	}

	setupLog.Info("Starting manager")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "Failed to run manager")
		os.Exit(1)
	}
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller.go
================================================
/*
Copyright 2022 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 controllers

import (
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Controller{}

// Controller scaffolds the file that defines the controller for a CRD or a builtin resource
//

type Controller struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin
	machinery.ProjectNameMixin
	machinery.NamespacedMixin

	ControllerRuntimeVersion string

	Force bool
}

// SetTemplateDefaults implements machinery.Template
func (f *Controller) SetTemplateDefaults() error {
	if f.Path == "" {
		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join("internal", "controller", "%[group]", "%[kind]_controller.go")
		} else {
			f.Path = filepath.Join("internal", "controller", "%[kind]_controller.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	f.TemplateBody = controllerTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.Error
	}

	return nil
}

//nolint:lll
const controllerTemplate = `{{ .Boilerplate }}

package {{ if and .MultiGroup .Resource.Group }}{{ .Resource.PackageName }}{{ else }}controller{{ end }}

import (
	"context"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	{{ if not (isEmptyStr .Resource.Path) -}}
	{{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
	{{- end }}
)

// {{ .Resource.Kind }}Reconciler reconciles a {{ .Resource.Kind }} object
type {{ .Resource.Kind }}Reconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

{{ if .Namespaced -}}
// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},namespace={{ .ProjectName }}-system,resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},namespace={{ .ProjectName }}-system,resources={{ .Resource.Plural }}/status,verbs=get;update;patch
// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},namespace={{ .ProjectName }}-system,resources={{ .Resource.Plural }}/finalizers,verbs=update
{{- else -}}
// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }},verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/status,verbs=get;update;patch
// +kubebuilder:rbac:groups={{ .Resource.QualifiedGroup }},resources={{ .Resource.Plural }}/finalizers,verbs=update
{{- end }}

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the {{ .Resource.Kind }} object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@{{ .ControllerRuntimeVersion }}/pkg/reconcile
func (r *{{ .Resource.Kind }}Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *{{ .Resource.Kind }}Reconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		{{ if not (isEmptyStr .Resource.Path) -}}
		For(&{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}).
		{{- else -}}
		// Uncomment the following line adding a pointer to an instance of the controlled resource as an argument
		// For().
		{{- end }}
		{{- if and (.MultiGroup) (not (isEmptyStr .Resource.Group)) }}
		Named("{{ lower .Resource.Group }}-{{ lower .Resource.Kind }}").
		{{- else }}
		Named("{{ lower .Resource.Kind }}").
		{{- end }}
		Complete(r)
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_suitetest.go
================================================
/*
Copyright 2022 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 controllers

import (
	"fmt"
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var (
	_ machinery.Template = &SuiteTest{}
	_ machinery.Inserter = &SuiteTest{}
)

// SuiteTest scaffolds the file that sets up the controller tests
//

type SuiteTest struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	// CRDDirectoryRelativePath define the Path for the CRD
	CRDDirectoryRelativePath string

	Force bool
}

// SetTemplateDefaults implements machinery.Template
func (f *SuiteTest) SetTemplateDefaults() error {
	if f.Path == "" {
		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join("internal", "controller", "%[group]", "suite_test.go")
		} else {
			f.Path = filepath.Join("internal", "controller", "suite_test.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	f.TemplateBody = fmt.Sprintf(controllerSuiteTestTemplate,
		machinery.NewMarkerFor(f.Path, importMarker),
		machinery.NewMarkerFor(f.Path, addSchemeMarker),
	)

	// If is multigroup the path needs to be ../../ since it has
	// the group dir.
	f.CRDDirectoryRelativePath = `"..",".."`
	if f.MultiGroup && f.Resource.Group != "" {
		f.CRDDirectoryRelativePath = `"..", "..",".."`
	}

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	}

	return nil
}

const (
	importMarker    = "imports"
	addSchemeMarker = "scheme"
)

// GetMarkers implements file.Inserter
func (f *SuiteTest) GetMarkers() []machinery.Marker {
	return []machinery.Marker{
		machinery.NewMarkerFor(f.Path, importMarker),
		machinery.NewMarkerFor(f.Path, addSchemeMarker),
	}
}

const (
	apiImportCodeFragment = `%s "%s"
`
	addschemeCodeFragment = `err = %s.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())

`
)

// GetCodeFragments implements file.Inserter
func (f *SuiteTest) GetCodeFragments() machinery.CodeFragmentsMap {
	fragments := make(machinery.CodeFragmentsMap, 2)

	// Generate import code fragments
	imports := make([]string, 0)
	if f.Resource.Path != "" {
		imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
	}

	// Generate add scheme code fragments
	addScheme := make([]string, 0)
	if f.Resource.Path != "" {
		addScheme = append(addScheme, fmt.Sprintf(addschemeCodeFragment, f.Resource.ImportAlias()))
	}

	// Only store code fragments in the map if the slices are non-empty
	if len(imports) != 0 {
		fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports
	}
	if len(addScheme) != 0 {
		fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme
	}

	return fragments
}

const controllerSuiteTestTemplate = `{{ .Boilerplate }}

{{if and .MultiGroup .Resource.Group }}
package {{ .Resource.PackageName }}
{{else}}
package controller
{{end}}

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	%s
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx context.Context
	cancel context.CancelFunc
	testEnv *envtest.Environment
	cfg *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	%s

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join({{ .CRDDirectoryRelativePath }}, "config", "crd", "bases")},
		ErrorIfCRDPathMissing: {{ .Resource.HasAPI }},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join({{ .CRDDirectoryRelativePath }}, "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/controllers/controller_test_template.go
================================================
/*
Copyright 2022 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 controllers

import (
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &ControllerTest{}

// ControllerTest scaffolds the file that sets up the controller unit tests
//

type ControllerTest struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	Force bool

	DoAPI bool
}

// SetTemplateDefaults implements machinery.Template
func (f *ControllerTest) SetTemplateDefaults() error {
	if f.Path == "" {
		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join("internal", "controller", "%[group]", "%[kind]_controller_test.go")
		} else {
			f.Path = filepath.Join("internal", "controller", "%[kind]_controller_test.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	f.TemplateBody = controllerTestTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	}

	return nil
}

const controllerTestTemplate = `{{ .Boilerplate }}

{{if and .MultiGroup .Resource.Group }}
package {{ .Resource.PackageName }}
{{else}}
package controller
{{end}}

import (
	{{ if .DoAPI -}}
	"context"
	{{- end }}
	. "github.com/onsi/ginkgo/v2"
	{{ if .DoAPI -}}

	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	{{ if not (isEmptyStr .Resource.Path) -}}
	{{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
	{{- end }}
	{{- end }}
)

var _ = Describe("{{ .Resource.Kind }} Controller", func() {
	Context("When reconciling a resource", func() {
		{{ if .DoAPI -}}
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default",  // TODO(user):Modify as needed
		}
		{{ lower .Resource.Kind }} := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind {{ .Resource.Kind }}")
			err := k8sClient.Get(ctx, typeNamespacedName, {{ lower .Resource.Kind }})
			if err != nil && errors.IsNotFound(err) {
				resource := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance {{ .Resource.Kind }}")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		{{- end }}
		It("should successfully reconcile the resource", func() {
			{{ if .DoAPI -}}
			By("Reconciling the created resource")
			controllerReconciler := &{{ .Resource.Kind }}Reconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			{{- end }}
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/customgcl.go
================================================
/*
Copyright 2026 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &CustomGcl{}

// CustomGcl scaffolds the .custom-gcl.yml file for golangci-lint module plugins
type CustomGcl struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// GolangciLintVersion is the version of golangci-lint to use
	GolangciLintVersion string
}

// SetTemplateDefaults implements machinery.Template
func (f *CustomGcl) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = ".custom-gcl.yml"
	}

	f.TemplateBody = customGclTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const customGclTemplate = `# This file configures golangci-lint with module plugins.
# When you run 'make lint', it will automatically build a custom golangci-lint binary
# with all the plugins listed below.
#
# See: https://golangci-lint.run/plugins/module-plugins/
version: {{ .GolangciLintVersion }}
plugins:
  # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs)
  - module: "sigs.k8s.io/logtools"
    import: "sigs.k8s.io/logtools/logcheck/gclplugin"
    version: latest
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/devcontainer.go
================================================
/*
Copyright 2024 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

// devContainerTemplate defines the devcontainer.json configuration
// Works with VS Code, GitHub Codespaces, and other devcontainer-compatible tools
//
// Configuration choices:
//   - moby: false - Uses Docker CE instead of Moby, fixes DinD issues in Codespaces
//   - dockerDefaultAddressPool - Prevents subnet conflicts in shared/cloud environments
//   - --privileged - Required for Docker daemon to run inside container (DinD)
//   - --init - Properly handles zombie processes and signal forwarding
//   - GO111MODULE=on - Ensures Go modules work consistently
//   - Runs as root (golang:1.25 default) - no sudo needed in post-install script
const devContainerTemplate = `{
  "name": "Kubebuilder DevContainer",
  "image": "golang:1.25",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {
      "moby": false,
      "dockerDefaultAddressPool": "base=172.30.0.0/16,size=24"
    },
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/common-utils:2": {
      "upgradePackages": true
    }
  },

  "runArgs": ["--privileged", "--init"],

  "customizations": {
    "vscode": {
      "settings": {
        "terminal.integrated.shell.linux": "/bin/bash"
      },
      "extensions": [
        "ms-kubernetes-tools.vscode-kubernetes-tools",
        "ms-azuretools.vscode-docker"
      ]
    }
  },

  "remoteEnv": {
    "GO111MODULE": "on"
  },

  "onCreateCommand": "bash .devcontainer/post-install.sh"
}

`

const postInstallScript = `#!/bin/bash
set -euo pipefail

echo "===================================="
echo "Kubebuilder DevContainer Setup"
echo "===================================="

# Verify running as root (required for installing to /usr/local/bin and /etc)
if [ "$(id -u)" -ne 0 ]; then
  echo "ERROR: This script must be run as root"
  exit 1
fi

echo ""
echo "Detecting system architecture..."
# Detect architecture using uname
MACHINE=$(uname -m)
case "${MACHINE}" in
  x86_64)
    ARCH="amd64"
    ;;
  aarch64|arm64)
    ARCH="arm64"
    ;;
  *)
    echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64"
    ARCH="amd64"
    ;;
esac
echo "Architecture: ${ARCH}"

echo ""
echo "------------------------------------"
echo "Setting up bash completion..."
echo "------------------------------------"

BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions"

# Enable bash-completion in root's .bashrc (devcontainer runs as root)
if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then
  echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc
  echo "Added bash-completion to .bashrc"
fi

echo ""
echo "------------------------------------"
echo "Installing development tools..."
echo "------------------------------------"

# Install kind
if ! command -v kind &> /dev/null; then
  echo "Installing kind..."
  curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}"
  chmod +x /usr/local/bin/kind
  echo "kind installed successfully"
fi

# Generate kind bash completion
if command -v kind &> /dev/null; then
  if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then
    echo "kind completion installed"
  else
    echo "WARNING: Failed to generate kind completion"
  fi
fi

# Install kubebuilder
if ! command -v kubebuilder &> /dev/null; then
  echo "Installing kubebuilder..."
  curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}"
  chmod +x /usr/local/bin/kubebuilder
  echo "kubebuilder installed successfully"
fi

# Generate kubebuilder bash completion
if command -v kubebuilder &> /dev/null; then
  if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then
    echo "kubebuilder completion installed"
  else
    echo "WARNING: Failed to generate kubebuilder completion"
  fi
fi

# Install kubectl
if ! command -v kubectl &> /dev/null; then
  echo "Installing kubectl..."
  KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt)
  curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl"
  chmod +x /usr/local/bin/kubectl
  echo "kubectl installed successfully"
fi

# Generate kubectl bash completion
if command -v kubectl &> /dev/null; then
  if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then
    echo "kubectl completion installed"
  else
    echo "WARNING: Failed to generate kubectl completion"
  fi
fi

# Generate Docker bash completion
if command -v docker &> /dev/null; then
  if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then
    echo "docker completion installed"
  else
    echo "WARNING: Failed to generate docker completion"
  fi
fi

echo ""
echo "------------------------------------"
echo "Configuring Docker environment..."
echo "------------------------------------"

# Wait for Docker to be ready
echo "Waiting for Docker to be ready..."
for i in {1..30}; do
  if docker info >/dev/null 2>&1; then
    echo "Docker is ready"
    break
  fi
  if [ "$i" -eq 30 ]; then
    echo "WARNING: Docker not ready after 30s"
  fi
  sleep 1
done

# Create kind network (ignore if already exists)
if ! docker network inspect kind >/dev/null 2>&1; then
  if docker network create kind >/dev/null 2>&1; then
    echo "Created kind network"
  else
    echo "WARNING: Failed to create kind network (may already exist)"
  fi
fi

echo ""
echo "------------------------------------"
echo "Verifying installations..."
echo "------------------------------------"
kind version
kubebuilder version
kubectl version --client
docker --version
go version

echo ""
echo "===================================="
echo "DevContainer ready!"
echo "===================================="
echo "All development tools installed successfully."
echo "You can now start building Kubernetes operators."
`

var (
	_ machinery.Template = &DevContainer{}
	_ machinery.Template = &DevContainerPostInstallScript{}
)

// DevContainer scaffoldds a `devcontainer.json` configurations file for creating Kubebuilder & Kind based DevContainer.
type DevContainer struct {
	machinery.TemplateMixin
}

// DevContainerPostInstallScript defines the scaffold that will be done with the post install script
type DevContainerPostInstallScript struct {
	machinery.TemplateMixin
}

// SetTemplateDefaults set defaults for this template
func (f *DevContainer) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = ".devcontainer/devcontainer.json"
	}

	f.TemplateBody = devContainerTemplate

	return nil
}

// SetTemplateDefaults set the defaults of this template
func (f *DevContainerPostInstallScript) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = ".devcontainer/post-install.sh"
	}

	f.TemplateBody = postInstallScript

	return nil
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/dockerfile.go
================================================
/*
Copyright 2022 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Dockerfile{}

// Dockerfile scaffolds a file that defines the containerized build process
type Dockerfile struct {
	machinery.TemplateMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *Dockerfile) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = "Dockerfile"
	}

	f.TemplateBody = dockerfileTemplate

	return nil
}

const dockerfileTemplate = `# Build the manager binary
FROM golang:1.25 AS builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

# Copy the Go source (relies on .dockerignore to filter)
COPY . .

# Build
# the GOARCH has no default value to allow the binary to be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/dockerignore.go
================================================
/*
Copyright 2022 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &DockerIgnore{}

// DockerIgnore scaffolds a file that defines which files should be ignored by the containerized build process
type DockerIgnore struct {
	machinery.TemplateMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *DockerIgnore) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = ".dockerignore"
	}

	f.TemplateBody = dockerignorefileTemplate

	return nil
}

const dockerignorefileTemplate = `# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore everything by default and re-include only needed files
**

# Re-include Go source files (but not *_test.go)
!**/*.go
**/*_test.go

# Re-include Go module files
!go.mod
!go.sum
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/github/lint.go
================================================
/*
Copyright 2024 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 github

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &TestCi{}

// LintCi scaffolds the GitHub Action to lint the project
type LintCi struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin

	// golangci-lint version to use in the project
	GolangciLintVersion string
}

// SetTemplateDefaults implements machinery.Template
func (f *LintCi) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join(".github", "workflows", "lint.yml")
	}

	f.TemplateBody = lintCiTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const lintCiTemplate = `name: Lint

on:
  push:
  pull_request:

jobs:
  lint:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Check linter configuration
        run: make lint-config
      - name: Run linter
        run: make lint
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/github/test-e2e.go
================================================
/*
Copyright 2024 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 github

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &E2eTestCi{}

// E2eTestCi scaffolds the GitHub Action to call make test-e2e
type E2eTestCi struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *E2eTestCi) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join(".github", "workflows", "test-e2e.yml")
	}

	f.TemplateBody = e2eTestCiTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const e2eTestCiTemplate = `name: E2E Tests

on:
  push:
  pull_request:

jobs:
  test-e2e:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install the latest version of kind
        run: |
          curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind

      - name: Verify kind installation
        run: kind version

      - name: Running Test e2e
        run: |
          go mod tidy
          make test-e2e
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/github/test.go
================================================
/*
Copyright 2024 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 github

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &TestCi{}

// TestCi scaffolds the GitHub Action to call make test
type TestCi struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *TestCi) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join(".github", "workflows", "test.yml")
	}

	f.TemplateBody = testCiTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const testCiTemplate = `name: Tests

on:
  push:
  pull_request:

jobs:
  test:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Running Tests
        run: |
          go mod tidy
          make test
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/gitignore.go
================================================
/*
Copyright 2022 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &GitIgnore{}

// GitIgnore scaffolds a file that defines which files should be ignored by git
type GitIgnore struct {
	machinery.TemplateMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *GitIgnore) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = ".gitignore"
	}

	f.TemplateBody = gitignoreTemplate

	return nil
}

const gitignoreTemplate = `# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
Dockerfile.cross

# Test binary, built with ` + "`go test -c`" + `
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Go workspace file
go.work

# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*

# editor and IDE paraphernalia
.idea
.vscode
*.swp
*.swo
*~

# Kubeconfig might contain secrets
*.kubeconfig
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/golangci.go
================================================
/*
Copyright 2022 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Golangci{}

// Golangci scaffolds a file which define Golangci rules
type Golangci struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *Golangci) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = ".golangci.yml"
	}

	f.TemplateBody = golangciTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const golangciTemplate = `version: "2"
run:
  allow-parallel-runners: true
linters:
  default: none
  enable:
    - copyloopvar
    - dupl
    - errcheck
    - ginkgolinter
    - goconst
    - gocyclo
    - govet
    - ineffassign
    - lll
    - modernize
    - misspell
    - nakedret
    - prealloc
    - revive
    - staticcheck
    - unconvert
    - unparam
    - unused
    - logcheck
  settings:
    custom:
      logcheck:
        type: "module"
        description: Checks Go logging calls for Kubernetes logging conventions.
    revive:
      rules:
        - name: comment-spacings
        - name: import-shadowing
    modernize:
      disable:
        - omitzero
  exclusions:
    generated: lax
    rules:
      - linters:
          - lll
        path: api/*
      - linters:
          - dupl
          - lll
        path: internal/*
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gofmt
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/gomod.go
================================================
/*
Copyright 2022 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &GoMod{}

// GoMod scaffolds a file that defines the project dependencies
type GoMod struct {
	machinery.TemplateMixin
	machinery.RepositoryMixin

	ControllerRuntimeVersion string
}

// SetTemplateDefaults implements machinery.Template
func (f *GoMod) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = "go.mod"
	}

	f.TemplateBody = goModTemplate

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const goModTemplate = `module {{ .Repo }}

go 1.25.3

require (
	sigs.k8s.io/controller-runtime {{ .ControllerRuntimeVersion }}
)
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/hack/boilerplate.go
================================================
/*
Copyright 2022 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 hack

import (
	"fmt"
	"path/filepath"
	"time"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

// DefaultBoilerplatePath is the default path to the boilerplate file
var DefaultBoilerplatePath = filepath.Join("hack", "boilerplate.go.txt")

var _ machinery.Template = &Boilerplate{}

// Boilerplate scaffolds a file that defines the common header for the rest of the files
type Boilerplate struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin

	// License is the License type to write
	License string

	// Licenses maps License types to their actual string
	Licenses map[string]string

	// Owner is the copyright owner - e.g. "The Kubernetes Authors"
	Owner string

	// Year is the copyright year
	Year string
}

// Validate implements file.RequiresValidation
func (f *Boilerplate) Validate() error {
	if f.License != "" {
		if _, foundKnown := knownLicenses[f.License]; !foundKnown {
			if _, found := f.Licenses[f.License]; !found {
				return fmt.Errorf("unknown specified license %s", f.License)
			}
		}
	}
	return nil
}

// SetTemplateDefaults implements machinery.Template
func (f *Boilerplate) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = DefaultBoilerplatePath
	}

	if f.License == "" {
		f.License = "apache2"
	}

	if f.Licenses == nil {
		f.Licenses = make(map[string]string, len(knownLicenses))
	}

	for key, value := range knownLicenses {
		if _, hasLicense := f.Licenses[key]; !hasLicense {
			f.Licenses[key] = value
		}
	}

	if f.Year == "" {
		f.Year = fmt.Sprintf("%v", time.Now().Year())
	}

	// Boilerplate given
	if len(f.Boilerplate) > 0 {
		f.TemplateBody = f.Boilerplate
		return nil
	}

	f.TemplateBody = boilerplateTemplate

	return nil
}

const boilerplateTemplate = `/*
{{ if .Owner -}}
Copyright {{ .Year }} {{ .Owner }}.
{{- else -}}
Copyright {{ .Year }}.
{{- end }}
{{ index .Licenses .License }}*/`

var knownLicenses = map[string]string{
	"apache2":   apache2,
	"copyright": "",
}

const apache2 = `
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: pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go
================================================
/*
Copyright 2022 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Makefile{}

// Makefile scaffolds a file that defines project management CLI commands
type Makefile struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// Image is controller manager image name
	Image string
	// BoilerplatePath is the path to the boilerplate file
	BoilerplatePath string
	// Controller tools version to use in the project
	ControllerToolsVersion string
	// Kustomize version to use in the project
	KustomizeVersion string
	// golangci-lint version to use in the project
	GolangciLintVersion string
	// ControllerRuntimeVersion version to be used to download the envtest setup script
	ControllerRuntimeVersion string
	// EnvtestVersion store the name of the verions to be used to install setup-envtest
	EnvtestVersion string
}

// SetTemplateDefaults implements machinery.Template
func (f *Makefile) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = "Makefile"
	}

	f.TemplateBody = makefileTemplate

	f.IfExistsAction = machinery.Error

	if f.Image == "" {
		f.Image = "controller:latest"
	}

	// TODO: Current workaround for setup-envtest compatibility
	// Due to past instances where controller-runtime maintainers released
	// versions without corresponding branches, directly relying on branches
	// poses a risk of breaking the Kubebuilder chain. Such practices may
	// change over time, potentially leading to compatibility issues. This
	// approach, although not ideal, remains the best solution for ensuring
	// compatibility with controller-runtime releases as of now. For more
	// details on the quest for a more robust solution, refer to the issue
	// raised in the controller-runtime repository: https://github.com/kubernetes-sigs/controller-runtime/issues/2744
	if f.EnvtestVersion == "" {
		f.EnvtestVersion = "latest"
	}
	return nil
}

//nolint:lll
const makefileTemplate = `# Image URL to use all building/pushing image targets
IMG ?= {{ .Image }}

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

# CONTAINER_TOOL defines the container tool to be used for building images.
# Be aware that the target commands are only tested with Docker which is
# scaffolded by default. However, you might want to replace it to use other
# tools. (i.e. podman)
CONTAINER_TOOL ?= docker

# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

.PHONY: all
all: build

##@ General

# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk command is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php

.PHONY: help
help: ## Display this help.
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Development

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
	"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
	{{ if .BoilerplatePath -}}
	"$(CONTROLLER_GEN)" object:headerFile={{printf "%q" .BoilerplatePath}} paths="./..."
	{{- else -}}
	"$(CONTROLLER_GEN)" object paths="./..."
	{{- end }}

.PHONY: fmt
fmt: ## Run go fmt against code.
	go fmt ./...

.PHONY: vet
vet: ## Run go vet against code.
	go vet ./...

.PHONY: test
test: manifests generate fmt vet setup-envtest ## Run tests.
	KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out

# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
# CertManager is installed by default; skip with:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= {{ .ProjectName }}-test-e2e

.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
	@command -v $(KIND) >/dev/null 2>&1 || { \
		echo "Kind is not installed. Please install Kind manually."; \
		exit 1; \
	}
	@case "$$($(KIND) get clusters)" in \
		*"$(KIND_CLUSTER)"*) \
			echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
		*) \
			echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
			$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
	esac

.PHONY: test-e2e
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
	KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
	$(MAKE) cleanup-test-e2e

.PHONY: cleanup-test-e2e
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
	@$(KIND) delete cluster --name $(KIND_CLUSTER)

.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
	"$(GOLANGCI_LINT)" run

.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
	"$(GOLANGCI_LINT)" run --fix

.PHONY: lint-config
lint-config: golangci-lint ## Verify golangci-lint linter configuration
	"$(GOLANGCI_LINT)" config verify

##@ Build

.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
	go build -o bin/manager cmd/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
	go run ./cmd/main.go

# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
	$(CONTAINER_TOOL) build -t ${IMG} .

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
	$(CONTAINER_TOOL) push ${IMG}

# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail)
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: ## Build and push docker image for the manager for cross-platform support
	# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
	sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
	- $(CONTAINER_TOOL) buildx create --name {{ .ProjectName }}-builder
	$(CONTAINER_TOOL) buildx use {{ .ProjectName }}-builder
	- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
	- $(CONTAINER_TOOL) buildx rm {{ .ProjectName }}-builder
	rm Dockerfile.cross

.PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
	mkdir -p dist
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default > dist/install.yaml

##@ Deployment

ifndef ignore-not-found
  ignore-not-found = false
endif

.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi

.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi

.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -

.PHONY: undeploy
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -

##@ Dependencies

## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
	mkdir -p "$(LOCALBIN)"

## Tool Binaries
KUBECTL ?= kubectl
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint

## Tool Versions
KUSTOMIZE_VERSION ?= {{ .KustomizeVersion }}
CONTROLLER_TOOLS_VERSION ?= {{ .ControllerToolsVersion }}

#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/')

#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')

GOLANGCI_LINT_VERSION ?= {{ .GolangciLintVersion }}
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
	$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))

.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
	$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))

.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
	@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
	@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
		echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
		exit 1; \
	}

.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
	$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
	$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
	@test -f .custom-gcl.yml && { \
		echo "Building custom golangci-lint with plugins..." && \
		$(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \
		mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \
	} || true

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f "$(1)" ;\
GOBIN="$(LOCALBIN)" go install $${package} ;\
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
} ;\
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
endef

define gomodver
$(shell go list -m -f '{{"{{"}}if .Replace{{"}}"}}{{"{{"}}.Replace.Version{{"}}"}}{{"{{"}}else{{"}}"}}{{"{{"}}.Version{{"}}"}}{{"{{"}}end{{"}}"}}' $(1) 2>/dev/null)
endef
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/readme.go
================================================
/*
Copyright 2022 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 templates

import (
	"fmt"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Readme{}

// Readme scaffolds a README.md file
type Readme struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin
	machinery.ProjectNameMixin

	License string

	// CommandName stores the name of the bin used
	CommandName string
}

// SetTemplateDefaults implements machinery.Template
func (f *Readme) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = "README.md"
	}

	f.License = strings.Replace(
		strings.Replace(f.Boilerplate, "/*", "", 1),
		"*/", "", 1)

	f.TemplateBody = fmt.Sprintf(readmeFileTemplate,
		codeFence("make docker-build docker-push IMG=/{{ .ProjectName }}:tag"),
		codeFence("make install"),
		codeFence("make deploy IMG=/{{ .ProjectName }}:tag"),
		codeFence("kubectl apply -k config/samples/"),
		codeFence("kubectl delete -k config/samples/"),
		codeFence("make uninstall"),
		codeFence("make undeploy"),
		codeFence("make build-installer IMG=/{{ .ProjectName }}:tag"),
		codeFence("kubectl apply -f https://raw.githubusercontent.com//{{ .ProjectName }}/"+
			"/dist/install.yaml"),
		codeFence(fmt.Sprintf("%s edit --plugins=helm/v2-alpha", f.CommandName)),
	)

	return nil
}

const readmeFileTemplate = `# {{ .ProjectName }}
// TODO(user): Add simple overview of use/purpose

## Description
// TODO(user): An in-depth paragraph about your project and overview of use

## Getting Started

### Prerequisites
- go version v1.24.6+
- docker version 17.03+.
- kubectl version v1.11.3+.
- Access to a Kubernetes v1.11.3+ cluster.

### To Deploy on the cluster
**Build and push your image to the location specified by ` + "`IMG`" + `:**

%s

**NOTE:** This image ought to be published in the personal registry you specified.
And it is required to have access to pull the image from the working environment.
Make sure you have the proper permission to the registry if the above commands don’t work.

**Install the CRDs into the cluster:**

%s

**Deploy the Manager to the cluster with the image specified by ` + "`IMG`" + `:**

%s

> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
privileges or be logged in as admin.

**Create instances of your solution**
You can apply the samples (examples) from the config/sample:

%s

>**NOTE**: Ensure that the samples has default values to test it out.

### To Uninstall
**Delete the instances (CRs) from the cluster:**

%s

**Delete the APIs(CRDs) from the cluster:**

%s

**UnDeploy the controller from the cluster:**

%s

## Project Distribution

Following the options to release and provide this solution to the users.

### By providing a bundle with all YAML files

1. Build the installer for the image built and published in the registry:

%s

**NOTE:** The makefile target mentioned above generates an 'install.yaml'
file in the dist directory. This file contains all the resources built
with Kustomize, which are necessary to install this project without its
dependencies.

2. Using the installer

Users can just run 'kubectl apply -f ' to install
the project, i.e.:

%s

### By providing a Helm Chart

1. Build the chart using the optional helm plugin

%s

2. See that a chart was generated under 'dist/chart', and users
can obtain this solution from there.

**NOTE:** If you change the project, you need to update the Helm Chart
using the same command above to sync the latest changes. Furthermore,
if you create webhooks, you need to use the above command with
the '--force' flag and manually ensure that any custom configuration
previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml'
is manually re-applied afterwards.

## Contributing
// TODO(user): Add detailed information on how you would like others to contribute to this project

**NOTE:** Run ` + "`make help`" + ` for more information on all potential ` + "`make`" + ` targets

More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)

## License
{{ .License }}
`

func codeFence(code string) string {
	return "```sh" + "\n" + code + "\n" + "```"
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/suite.go
================================================
/*
Copyright 2022 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 e2e

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &SuiteTest{}

// SuiteTest scaffolds the files for the e2e tests
type SuiteTest struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin
	machinery.RepositoryMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *SuiteTest) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = "test/e2e/e2e_suite_test.go"
	}

	f.TemplateBody = suiteTestTemplate
	return nil
}

var suiteTestTemplate = `//go:build e2e
// +build e2e

{{ .Boilerplate }}

package e2e

import (
	"fmt"
	"os"
	"os/exec"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"{{ .Repo }}/test/utils"
)

var (
	// managerImage is the manager image to be built and loaded for testing.
	managerImage = "example.com/{{ .ProjectName }}:v0.0.1"
	// shouldCleanupCertManager tracks whether CertManager was installed by this suite.
	shouldCleanupCertManager = false
)

// TestE2E runs the e2e test suite to validate the solution in an isolated environment.
// The default setup requires Kind and CertManager.
//
// To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true
func TestE2E(t *testing.T) {
	RegisterFailHandler(Fail)
	_, _ = fmt.Fprintf(GinkgoWriter, "Starting {{ .ProjectName }} e2e test suite\n")
	RunSpecs(t, "e2e suite")
}

var _ = BeforeSuite(func() {
	By("building the manager image")
	cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage))
	_, err := utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image")

	// TODO(user): If you want to change the e2e test vendor from Kind,
	// ensure the image is built and available, then remove the following block.
	By("loading the manager image on Kind")
	err = utils.LoadImageToKindClusterWithName(managerImage)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind")

	setupCertManager()
})

var _ = AfterSuite(func() {
	teardownCertManager()
})

// setupCertManager installs CertManager if needed for webhook tests.
// Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present.
func setupCertManager() {
	if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n")
		return
	}

	By("checking if CertManager is already installed")
	if utils.IsCertManagerCRDsInstalled() {
		_, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n")
		return
	}

	// Mark for cleanup before installation to handle interruptions and partial installs.
	shouldCleanupCertManager = true

	By("installing CertManager")
	Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
}

// teardownCertManager uninstalls CertManager if it was installed by setupCertManager.
// This ensures we only remove what we installed.
func teardownCertManager() {
	if !shouldCleanupCertManager {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n")
		return
	}

	By("uninstalling CertManager")
	utils.UninstallCertManager()
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e/test.go
================================================
/*
Copyright 2022 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 e2e

import (
	"bytes"
	"fmt"
	log "log/slog"
	"os"
	"path/filepath"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var (
	_ machinery.Template = &Test{}
	_ machinery.Inserter = &WebhookTestUpdater{}
)

const (
	webhookChecksMarker           = "e2e-webhooks-checks"
	metricsWebhookReadinessMarker = "e2e-metrics-webhooks-readiness"
)

// Test defines the basic setup for the e2e test
type Test struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin
	machinery.RepositoryMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults set defaults for this template
func (f *Test) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("test", "e2e", "e2e_test.go")
	}

	// This is where the template body is defined with markers
	f.TemplateBody = testCodeTemplate

	return nil
}

// WebhookTestUpdater updates e2e_test.go to insert additional webhook validation tests
type WebhookTestUpdater struct {
	machinery.RepositoryMixin
	machinery.ProjectNameMixin
	machinery.ResourceMixin
	WireWebhook bool
}

// GetPath implements file.Builder
func (*WebhookTestUpdater) GetPath() string {
	return filepath.Join("test", "e2e", "e2e_test.go")
}

// GetIfExistsAction implements file.Builder
func (*WebhookTestUpdater) GetIfExistsAction() machinery.IfExistsAction {
	return machinery.OverwriteFile // Ensures only the marker is replaced
}

// GetMarkers implements file.Inserter
func (f *WebhookTestUpdater) GetMarkers() []machinery.Marker {
	return []machinery.Marker{
		machinery.NewMarkerFor(f.GetPath(), webhookChecksMarker),
		machinery.NewMarkerFor(f.GetPath(), metricsWebhookReadinessMarker),
	}
}

// GetCodeFragments implements file.Inserter
func (f *WebhookTestUpdater) GetCodeFragments() machinery.CodeFragmentsMap {
	// Check if any webhook type exists (defaulting, validation, or conversion)
	hasAnyWebhook := f.WireWebhook || (f.Resource != nil && f.Resource.HasConversionWebhook())

	if !hasAnyWebhook {
		return nil
	}

	filePath := f.GetPath()

	content, err := os.ReadFile(filePath)
	if err != nil {
		log.Warn("Unable to read file", "file", filePath, "error", err)
		log.Warn("Webhook test code injection will be skipped for this file.")
		log.Warn("This typically occurs when the file was removed and is missing.")
		log.Warn("If you intend to scaffold webhook tests, ensure the file and its markers exist.")
		return nil
	}

	codeFragments := machinery.CodeFragmentsMap{}
	markers := f.GetMarkers()

	for _, marker := range markers {
		markerStr := marker.String()
		if !bytes.Contains(content, []byte(markerStr)) {
			log.Warn("Marker not found in file, skipping webhook test code injection",
				"marker", markerStr,
				"file_path", filePath)
			continue // skip this marker
		}

		switch {
		case strings.Contains(markerStr, webhookChecksMarker):
			var fragments []string
			fragments = append(fragments, webhookChecksFragment)

			if f.Resource != nil && f.Resource.HasDefaultingWebhook() {
				mutatingWebhookCode := fmt.Sprintf(mutatingWebhookChecksFragment, f.ProjectName)
				fragments = append(fragments, mutatingWebhookCode)
			}

			if f.Resource != nil && f.Resource.HasValidationWebhook() {
				validatingWebhookCode := fmt.Sprintf(validatingWebhookChecksFragment, f.ProjectName)
				fragments = append(fragments, validatingWebhookCode)
			}

			if f.Resource != nil && f.Resource.HasConversionWebhook() {
				conversionWebhookCode := fmt.Sprintf(
					conversionWebhookChecksFragment,
					f.Resource.Kind,
					f.Resource.Plural+"."+f.Resource.Group+"."+f.Resource.Domain,
				)
				fragments = append(fragments, conversionWebhookCode)
			}

			if len(fragments) > 0 {
				codeFragments[marker] = fragments
			}
		case strings.Contains(markerStr, metricsWebhookReadinessMarker):
			// Readiness checks only for defaulting/validation webhooks (they use webhook service)
			// Conversion webhooks don't need separate webhook service readiness checks
			if f.WireWebhook {
				// Skip if webhook readiness checks are already present
				// This prevents duplicate insertion when multiple webhooks are scaffolded
				if strings.Contains(string(content), "waiting for the webhook service endpoints to be ready") {
					continue
				}

				webhookServiceName := fmt.Sprintf("%s-webhook-service", f.ProjectName)
				var fragments []string

				// Add endpoint readiness check (applies to all webhook types)
				fragments = append(fragments, fmt.Sprintf(webhookEndpointsReadinessFragment, webhookServiceName))

				// Add mutating webhook configuration check if defaulting webhooks exist
				if f.Resource != nil && f.Resource.HasDefaultingWebhook() {
					fragments = append(fragments, fmt.Sprintf(mutatingWebhookReadinessFragment, f.ProjectName))
				}

				// Add validating webhook configuration check if validation webhooks exist
				if f.Resource != nil && f.Resource.HasValidationWebhook() {
					fragments = append(fragments, fmt.Sprintf(validatingWebhookReadinessFragment, f.ProjectName))
				}

				// Add stabilization wait at the end
				fragments = append(fragments, webhookStabilizationFragment)

				if len(fragments) > 0 {
					codeFragments[marker] = fragments
				}
			}
		}
	}

	if len(codeFragments) == 0 {
		return nil
	}

	return codeFragments
}

const webhookChecksFragment = `It("should provisioned cert-manager", func() {
	By("validating that cert-manager has the certificate Secret")
	verifyCertManager := func(g Gomega) {
		cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace)
		_, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred())
	}
	Eventually(verifyCertManager).Should(Succeed())
})

`

const mutatingWebhookChecksFragment = `It("should have CA injection for mutating webhooks", func() {
	By("checking CA injection for mutating webhooks")
	verifyCAInjection := func(g Gomega) {
		cmd := exec.Command("kubectl", "get",
			"mutatingwebhookconfigurations.admissionregistration.k8s.io",
			"%s-mutating-webhook-configuration",
			"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
		mwhOutput, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred())
		g.Expect(len(mwhOutput)).To(BeNumerically(">", 10))
	}
	Eventually(verifyCAInjection).Should(Succeed())
})

`

const validatingWebhookChecksFragment = `It("should have CA injection for validating webhooks", func() {
	By("checking CA injection for validating webhooks")
	verifyCAInjection := func(g Gomega) {
		cmd := exec.Command("kubectl", "get",
			"validatingwebhookconfigurations.admissionregistration.k8s.io",
			"%s-validating-webhook-configuration",
			"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
		vwhOutput, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred())
		g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
	}
	Eventually(verifyCAInjection).Should(Succeed())
})

`

const conversionWebhookChecksFragment = `It("should have CA injection for %[1]s conversion webhook", func() {
	By("checking CA injection for %[1]s conversion webhook")
	verifyCAInjection := func(g Gomega) {
		cmd := exec.Command("kubectl", "get",
			"customresourcedefinitions.apiextensions.k8s.io",
			"%[2]s",
			"-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}")
		vwhOutput, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred())
		g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
	}
	Eventually(verifyCAInjection).Should(Succeed())
})

`

const webhookEndpointsReadinessFragment = `By("waiting for the webhook service endpoints to be ready")
	verifyWebhookEndpointsReady := func(g Gomega) {
		cmd := exec.Command("kubectl", "get", "endpointslices.discovery.k8s.io", "-n", namespace,
			"-l", "kubernetes.io/service-name=%s",
			"-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}")
		output, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist")
		g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready")
	}
	Eventually(verifyWebhookEndpointsReady, 3*time.Minute, time.Second).Should(Succeed())

`

const mutatingWebhookReadinessFragment = `By("verifying the mutating webhook server is ready")
	verifyMutatingWebhookReady := func(g Gomega) {
		cmd := exec.Command("kubectl", "get", "mutatingwebhookconfigurations.admissionregistration.k8s.io",
			"%s-mutating-webhook-configuration",
			"-o", "jsonpath={.webhooks[0].clientConfig.caBundle}")
		output, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred(), "MutatingWebhookConfiguration should exist")
		g.Expect(output).ShouldNot(BeEmpty(), "Mutating webhook CA bundle not yet injected")
	}
	Eventually(verifyMutatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed())

`

const validatingWebhookReadinessFragment = `By("verifying the validating webhook server is ready")
	verifyValidatingWebhookReady := func(g Gomega) {
		cmd := exec.Command("kubectl", "get", "validatingwebhookconfigurations.admissionregistration.k8s.io",
			"%s-validating-webhook-configuration",
			"-o", "jsonpath={.webhooks[0].clientConfig.caBundle}")
		output, err := utils.Run(cmd)
		g.Expect(err).NotTo(HaveOccurred(), "ValidatingWebhookConfiguration should exist")
		g.Expect(output).ShouldNot(BeEmpty(), "Validating webhook CA bundle not yet injected")
	}
	Eventually(verifyValidatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed())

`

const webhookStabilizationFragment = `By("waiting additional time for webhook server to stabilize")
	time.Sleep(5 * time.Second)

`

var testCodeTemplate = `//go:build e2e
// +build e2e

{{ .Boilerplate }}

package e2e

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"{{ .Repo }}/test/utils"
)

// namespace where the project is deployed in
const namespace = "{{ .ProjectName }}-system"
// serviceAccountName created for the project
const serviceAccountName = "{{ .ProjectName }}-controller-manager"
// metricsServiceName is the name of the metrics service of the project
const metricsServiceName = "{{ .ProjectName }}-controller-manager-metrics-service"
// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
const metricsRoleBindingName = "{{ .ProjectName }}-metrics-binding"

var _ = Describe("Manager", Ordered, func() {
	var controllerPodName string

	// Before running the tests, set up the environment by creating the namespace,
	// enforce the restricted security policy to the namespace, installing CRDs,
	// and deploying the controller.
	BeforeAll(func() {
		By("creating manager namespace")
		cmd := exec.Command("kubectl", "create", "ns", namespace)
		_, err := utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")

		By("labeling the namespace to enforce the restricted security policy")
		cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
			"pod-security.kubernetes.io/enforce=restricted")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")

		By("installing CRDs")
		cmd = exec.Command("make", "install")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")

		By("deploying the controller-manager")
		cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage))
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
	})

	// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
	// and deleting the namespace.
	AfterAll(func() {
		By("cleaning up the curl pod for metrics")
		cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
		_, _ = utils.Run(cmd)

		By("undeploying the controller-manager")
		cmd = exec.Command("make", "undeploy")
		_, _ = utils.Run(cmd)

		By("uninstalling CRDs")
		cmd = exec.Command("make", "uninstall")
		_, _ = utils.Run(cmd)

		By("removing manager namespace")
		cmd = exec.Command("kubectl", "delete", "ns", namespace)
		_, _ = utils.Run(cmd)
	})

	// After each test, check for failures and collect logs, events,
	// and pod descriptions for debugging.
	AfterEach(func() {
		specReport := CurrentSpecReport()
		if specReport.Failed() {
			By("Fetching controller manager pod logs")
			cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
			controllerLogs, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
			}

			By("Fetching Kubernetes events")
			cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
			eventsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
			}

			By("Fetching curl-metrics logs")
			cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
			metricsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
			}

			By("Fetching controller manager pod description")
			cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
			podDescription, err := utils.Run(cmd)
			if err == nil {
				fmt.Println("Pod description:\n", podDescription)
			} else {
				fmt.Println("Failed to describe controller pod")
			}
		}
	})

	SetDefaultEventuallyTimeout(2 * time.Minute)
	SetDefaultEventuallyPollingInterval(time.Second)

	Context("Manager", func() {
		It("should run successfully", func() {
			By("validating that the controller-manager pod is running as expected")
			verifyControllerUp := func(g Gomega) {
				// Get the name of the controller-manager pod
				cmd := exec.Command("kubectl", "get",
					"pods", "-l", "control-plane=controller-manager",
					"-o", "go-template={{"{{"}} range .items {{"}}"}}" +
					"{{"{{"}} if not .metadata.deletionTimestamp {{"}}"}}" +
					"{{"{{"}} .metadata.name {{"}}"}}"+
					"{{"{{"}} \"\\n\" {{"}}"}}{{"{{"}} end {{"}}"}}{{"{{"}} end {{"}}"}}",
					"-n", namespace,
				)

				podOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
				podNames := utils.GetNonEmptyLines(podOutput)
				g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
				controllerPodName = podNames[0]
				g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))

				// Validate the pod's status
				cmd = exec.Command("kubectl", "get",
					"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
					"-n", namespace,
				)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
			}
			Eventually(verifyControllerUp).Should(Succeed())
		})

		It("should ensure the metrics endpoint is serving metrics", func() {
			By("creating a ClusterRoleBinding for the service account to allow access to metrics")
			cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
				"--clusterrole={{ .ProjectName}}-metrics-reader",
				fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
			)
			_, err := utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")

			By("validating that the metrics service is available")
			cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")

			By("getting the service account token")
			token, err := serviceAccountToken()
			Expect(err).NotTo(HaveOccurred())
			Expect(token).NotTo(BeEmpty())

			By("ensuring the controller pod is ready")
			verifyControllerPodReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace,
					"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("True"), "Controller pod not ready")
			}
			Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying that the controller manager is serving the metrics server")
			verifyMetricsServerStarted := func(g Gomega) {
				cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(ContainSubstring("Serving metrics server"),
 					"Metrics server not yet started")
			}
			Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed())

			// +kubebuilder:scaffold:e2e-metrics-webhooks-readiness

			By("creating the curl-metrics pod to access the metrics endpoint")
			cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
				"--namespace", namespace,
				"--image=curlimages/curl:latest",
				"--overrides",
				fmt.Sprintf(` + "`" + `{
					"spec": {
						"containers": [{
							"name": "curl",
							"image": "curlimages/curl:latest",
							"command": ["/bin/sh", "-c"],
							"args": [
								"for i in $(seq 1 30); do ` +
	`curl -v -k -H 'Authorization: Bearer %s' ` +
	`https://%s.%s.svc.cluster.local:8443/metrics ` +
	`&& exit 0 || sleep 2; done; exit 1"
							],
							"securityContext": {
								"readOnlyRootFilesystem": true,
								"allowPrivilegeEscalation": false,
								"capabilities": {
									"drop": ["ALL"]
								},
								"runAsNonRoot": true,
								"runAsUser": 1000,
								"seccompProfile": {
									"type": "RuntimeDefault"
								}
							}
						}],
						"serviceAccountName": "%s"
					}
				}` + "`" + `, token, metricsServiceName, namespace, serviceAccountName))
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")

			By("waiting for the curl-metrics pod to complete.")
			verifyCurlUp := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
					"-o", "jsonpath={.status.phase}",
					"-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
			}
			Eventually(verifyCurlUp, 5 * time.Minute).Should(Succeed())

			By("getting the metrics by checking curl-metrics logs")
			verifyMetricsAvailable := func(g Gomega) {
				metricsOutput, err := getMetricsOutput()
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
				g.Expect(metricsOutput).NotTo(BeEmpty())
				g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
			}
			Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
		})

		// +kubebuilder:scaffold:e2e-webhooks-checks

		// TODO: Customize the e2e test suite with scenarios specific to your project.
		// Consider applying sample/CR(s) and check their status and/or verifying
		// the reconciliation by using the metrics, i.e.:
		// metricsOutput, err := getMetricsOutput()
		// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
		// Expect(metricsOutput).To(ContainSubstring(
		//    fmt.Sprintf(` + "`controller_runtime_reconcile_total{controller=\"%s\",result=\"success\"} 1`" + `,
		//    strings.ToLower(),
		// ))
	})
})

// serviceAccountToken returns a token for the specified service account in the given namespace.
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
// and parsing the resulting token from the API response.
func serviceAccountToken() (string, error) {
	const tokenRequestRawString = ` + "`" + `{
		"apiVersion": "authentication.k8s.io/v1",
		"kind": "TokenRequest"
	}` + "`" + `

	// Temporary file to store the token request
	secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
	tokenRequestFile := filepath.Join("/tmp", secretName)
	err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
	if err != nil {
		return "", err
	}

	var out string
	verifyTokenCreation := func(g Gomega) {
		// Execute kubectl command to create the token
		cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
			"/api/v1/namespaces/%s/serviceaccounts/%s/token",
			namespace,
			serviceAccountName,
		), "-f", tokenRequestFile)

		output, err := cmd.CombinedOutput()
		g.Expect(err).NotTo(HaveOccurred())

		// Parse the JSON output to extract the token
		var token tokenRequest
		err = json.Unmarshal(output, &token)
		g.Expect(err).NotTo(HaveOccurred())

		out = token.Status.Token
	}
	Eventually(verifyTokenCreation).Should(Succeed())

	return out, err
}

// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
func getMetricsOutput() (string, error) {
	By("getting the curl-metrics logs")
	cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
	return utils.Run(cmd)
}

// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
// containing only the token field that we need to extract.
type tokenRequest struct {
	Status struct {
		Token string ` + "`json:\"token\"`" + `
	} ` + "`json:\"status\"`" + `
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/test/utils/utils.go
================================================
/*
Copyright 2022 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 utils

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Utils{}

// Utils define the template for the utils file
type Utils struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults set the defaults for its template
func (f *Utils) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = "test/utils/utils.go"
	}

	f.TemplateBody = utilsTemplate

	return nil
}

var utilsTemplate = `{{ .Boilerplate }}

package utils

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"

	. "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
)

const (
	certmanagerVersion = "v1.20.0"
	certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
	
	defaultKindBinary = "kind"
	defaultKindCluster = "kind"
)

func warnError(err error) {
	_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
}

// Run executes the provided command within this context
func Run(cmd *exec.Cmd) (string, error) {
	dir, _ := GetProjectDir()
	cmd.Dir = dir

	if err := os.Chdir(cmd.Dir); err != nil {
		_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
	}

	cmd.Env = append(os.Environ(), "GO111MODULE=on")
	command := strings.Join(cmd.Args, " ")
	_, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
	}

	return string(output), nil
}

// UninstallCertManager uninstalls the cert manager
func UninstallCertManager() {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "delete", "-f", url)
	if _, err := Run(cmd); err != nil {
		warnError(err)
	}

	// Delete leftover leases in kube-system (not cleaned by default)
	kubeSystemLeases := []string{
		"cert-manager-cainjector-leader-election",
		"cert-manager-controller",
	}
	for _, lease := range kubeSystemLeases {
		cmd = exec.Command("kubectl", "delete", "lease", lease,
			"-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
		if _, err := Run(cmd); err != nil {
			warnError(err)
		}
	}
}

// InstallCertManager installs the cert manager bundle.
func InstallCertManager() error {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "apply", "-f", url)
	if _, err := Run(cmd); err != nil {
		return err
	}
	// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
	// was re-installed after uninstalling on a cluster.
	cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
		"--for", "condition=Available",
		"--namespace", "cert-manager",
		"--timeout", "5m",
	)

	_, err := Run(cmd)
	return err
}

// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
// by verifying the existence of key CRDs related to Cert Manager.
func IsCertManagerCRDsInstalled() bool {
	// List of common Cert Manager CRDs
	certManagerCRDs := []string{
		"certificates.cert-manager.io",
		"issuers.cert-manager.io",
		"clusterissuers.cert-manager.io",
		"certificaterequests.cert-manager.io",
		"orders.acme.cert-manager.io",
		"challenges.acme.cert-manager.io",
	}

	// Execute the kubectl command to get all CRDs
	cmd := exec.Command("kubectl", "get", "crds")
	output, err := Run(cmd)
	if err != nil {
		return false
	}

	// Check if any of the Cert Manager CRDs are present
	crdList := GetNonEmptyLines(output)
	for _, crd := range certManagerCRDs {
		for _, line := range crdList {
			if strings.Contains(line, crd) {
				return true
			}
		}
	}

	return false
}

// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
func LoadImageToKindClusterWithName(name string) error {
	cluster := defaultKindCluster
	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
		cluster = v
	}
	kindOptions := []string{"load", "docker-image", name, "--name", cluster}
	kindBinary := defaultKindBinary
	if v, ok := os.LookupEnv("KIND"); ok {
		kindBinary = v
	}
	cmd := exec.Command(kindBinary, kindOptions...)
	_, err := Run(cmd)
	return err
}

// GetNonEmptyLines converts given command output string into individual objects
// according to line breakers, and ignores the empty elements in it.
func GetNonEmptyLines(output string) []string {
	var res []string
	elements := strings.SplitSeq(output, "\n")
	for element := range elements {
		if element != "" {
			res = append(res, element)
		}
	}

	return res
}

// GetProjectDir will return the directory where the project is
func GetProjectDir() (string, error) {
	wd, err := os.Getwd()
	if err != nil {
		return wd, fmt.Errorf("failed to get current working directory: %w", err)
	}
	wd = strings.ReplaceAll(wd, "/test/e2e", "")
	return wd, nil
}

// UncommentCode searches for target in the file and remove the comment prefix
// of the target content. The target content may span multiple lines.
func UncommentCode(filename, target, prefix string) error {
	// false positive
	// nolint:gosec
	content, err := os.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("failed to read file %q: %w", filename, err)
	}
	strContent := string(content)

	idx := strings.Index(strContent, target)
	if idx < 0 {
		return fmt.Errorf("unable to find the code %q to be uncommented", target)
	}

	out := new(bytes.Buffer)
	_, err = out.Write(content[:idx])
	if err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	scanner := bufio.NewScanner(bytes.NewBufferString(target))
	if !scanner.Scan() {
		return nil
	}
	for {
		if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
		// Avoid writing a newline in case the previous line was the last in target.
		if !scanner.Scan() {
			break
		}
		if _, err = out.WriteString("\n"); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
	}

	if _, err = out.Write(content[idx+len(target):]); err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	// false positive
	// nolint:gosec
	if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
		return fmt.Errorf("failed to write file %q: %w", filename, err)
	}

	return nil
}
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook.go
================================================
/*
Copyright 2022 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 webhooks

import (
	log "log/slog"
	"path/filepath"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Webhook{}

// Webhook scaffolds the file that defines a webhook for a CRD or a builtin resource
type Webhook struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	// Is the Group domain for the Resource replacing '.' with '-'
	QualifiedGroupWithDash string

	// Define value for AdmissionReviewVersions marker
	AdmissionReviewVersions string

	Force bool

	// Deprecated - The flag should be removed from go/v5
	// IsLegacyPath indicates if webhooks should be scaffolded under the API.
	// Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
	// This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
	IsLegacyPath bool
}

// SetTemplateDefaults implements machinery.Template
func (f *Webhook) SetTemplateDefaults() error {
	if f.Path == "" {
		// Deprecated: Remove me when remove go/v4
		baseDir := "api"
		if !f.IsLegacyPath {
			baseDir = filepath.Join("internal", "webhook")
		}

		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join(baseDir, "%[group]", "%[version]", "%[kind]_webhook.go")
		} else {
			f.Path = filepath.Join(baseDir, "%[version]", "%[kind]_webhook.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	webhookTemplate := webhookTemplate
	if f.Resource.HasDefaultingWebhook() {
		webhookTemplate = webhookTemplate + defaultingWebhookTemplate
	}
	if f.Resource.HasValidationWebhook() {
		webhookTemplate = webhookTemplate + validatingWebhookTemplate
	}
	f.TemplateBody = webhookTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.Error
	}

	f.AdmissionReviewVersions = "v1"
	f.QualifiedGroupWithDash = strings.ReplaceAll(f.Resource.QualifiedGroup(), ".", "-")

	return nil
}

const (
	webhookTemplate = `{{ .Boilerplate }}

package {{ .Resource.Version }}

import (
	{{- if or .Resource.HasValidationWebhook .Resource.HasDefaultingWebhook }}
	"context"
	{{- end }}

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	{{- if .Resource.HasValidationWebhook }}
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
	{{- end }}
	{{ if not .IsLegacyPath -}}
	{{ if not (isEmptyStr .Resource.Path) -}}
	{{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
	{{- end }}
	{{- end }}
)

// nolint:unused
// log is for logging in this package.
var {{ lower .Resource.Kind }}log = logf.Log.WithName("{{ lower .Resource.Kind }}-resource")

{{- if .IsLegacyPath -}}
// SetupWebhookWithManager will setup the manager to manage the webhooks.
func (r *{{ .Resource.Kind }}) SetupWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, r).
		{{- if .Resource.HasValidationWebhook }}
		WithValidator(&{{ .Resource.Kind }}CustomValidator{}).
		{{- if ne .Resource.Webhooks.ValidationPath "" }}
		WithValidatorCustomPath("{{ .Resource.Webhooks.ValidationPath }}").
		{{- end }}
		{{- end }}
		{{- if .Resource.HasDefaultingWebhook }}
		WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}).
		{{- if ne .Resource.Webhooks.DefaultingPath "" }}
		WithDefaulterCustomPath("{{ .Resource.Webhooks.DefaultingPath }}").
		{{- end }}
		{{- end }}
		Complete()
}
{{- else }}
// Setup{{ .Resource.Kind }}WebhookWithManager registers the webhook for {{ .Resource.Kind }} in the manager.
func Setup{{ .Resource.Kind }}WebhookWithManager(mgr ctrl.Manager) error {
	{{- if not (isEmptyStr .Resource.ImportAlias) }}
	return ctrl.NewWebhookManagedBy(mgr, &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}).
	{{- else }}
	return ctrl.NewWebhookManagedBy(mgr, &{{ .Resource.Kind }}{}).
	{{- end }}
		{{- if .Resource.HasValidationWebhook }}
		WithValidator(&{{ .Resource.Kind }}CustomValidator{}).
		{{- if ne .Resource.Webhooks.ValidationPath "" }}
		WithValidatorCustomPath("{{ .Resource.Webhooks.ValidationPath }}").
		{{- end }}
		{{- end }}
		{{- if .Resource.HasDefaultingWebhook }}
		WithDefaulter(&{{ .Resource.Kind }}CustomDefaulter{}).
		{{- if ne .Resource.Webhooks.DefaultingPath "" }}
		WithDefaulterCustomPath("{{ .Resource.Webhooks.DefaultingPath }}").
		{{- end }}
		{{- end }}
		Complete()
}
{{- end }}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
`

	//nolint:lll
	defaultingWebhookTemplate = `
// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}{{- if ne .Resource.Webhooks.DefaultingPath "" -}}path={{ .Resource.Webhooks.DefaultingPath }}{{- else -}}path=/mutate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{- end -}},mutating=true,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=m{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }}

{{ if .IsLegacyPath -}}
// +kubebuilder:object:generate=false
{{- end }}
// {{ .Resource.Kind }}CustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind {{ .Resource.Kind }} when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type {{ .Resource.Kind }}CustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind {{ .Resource.Kind }}.
{{- if .IsLegacyPath }}
func (d *{{ .Resource.Kind }}CustomDefaulter) Default(_ context.Context, obj *{{ .Resource.Kind }}) error {
	{{ lower .Resource.Kind }}log.Info("Defaulting for {{ .Resource.Kind }}", "name", obj.GetName())
{{- else }}
func (d *{{ .Resource.Kind }}CustomDefaulter) Default(_ context.Context, obj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) error {
	{{ lower .Resource.Kind }}log.Info("Defaulting for {{ .Resource.Kind }}", "name", obj.GetName())
{{- end }}

	// TODO(user): fill in your defaulting logic.

	return nil
}
`

	//nolint:lll
	validatingWebhookTemplate = `
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:{{ if ne .Resource.Webhooks.WebhookVersion "v1" }}webhookVersions={{"{"}}{{ .Resource.Webhooks.WebhookVersion }}{{"}"}},{{ end }}{{- if ne .Resource.Webhooks.ValidationPath "" -}}path={{ .Resource.Webhooks.ValidationPath }}{{- else -}}path=/validate-{{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}-{{ else }}{{ .QualifiedGroupWithDash }}-{{ end }}{{ .Resource.Version }}-{{ lower .Resource.Kind }}{{- end -}},mutating=false,failurePolicy=fail,sideEffects=None,groups={{ if and .Resource.Core (eq .Resource.QualifiedGroup "core") }}""{{ else }}{{ .Resource.QualifiedGroup }}{{ end }},resources={{ .Resource.Plural }},verbs=create;update,versions={{ .Resource.Version }},name=v{{ lower .Resource.Kind }}-{{ .Resource.Version }}.kb.io,admissionReviewVersions={{ .AdmissionReviewVersions }}

{{ if .IsLegacyPath -}}
// +kubebuilder:object:generate=false
{{- end }}
// {{ .Resource.Kind }}CustomValidator struct is responsible for validating the {{ .Resource.Kind }} resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type {{ .Resource.Kind }}CustomValidator struct{
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}.
{{- if .IsLegacyPath }}
func (v *{{ .Resource.Kind }}CustomValidator) ValidateCreate(_ context.Context, obj *{{ .Resource.Kind }}) (admission.Warnings, error) {
	{{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon creation", "name", obj.GetName())
{{- else }}
func (v *{{ .Resource.Kind }}CustomValidator) ValidateCreate(_ context.Context, obj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) (admission.Warnings, error) {
	{{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon creation", "name", obj.GetName())
{{- end }}

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}.
{{- if .IsLegacyPath }}
func (v *{{ .Resource.Kind }}CustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *{{ .Resource.Kind }}) (admission.Warnings, error) {
	{{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon update", "name", newObj.GetName())
{{- else }}
func (v *{{ .Resource.Kind }}CustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) (admission.Warnings, error) {
	{{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon update", "name", newObj.GetName())
{{- end }}

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type {{ .Resource.Kind }}.
{{- if .IsLegacyPath }}
func (v *{{ .Resource.Kind }}CustomValidator) ValidateDelete(_ context.Context, obj *{{ .Resource.Kind }}) (admission.Warnings, error) {
	{{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon deletion", "name", obj.GetName())
{{- else }}
func (v *{{ .Resource.Kind }}CustomValidator) ValidateDelete(_ context.Context, obj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}) (admission.Warnings, error) {
	{{ lower .Resource.Kind }}log.Info("Validation for {{ .Resource.Kind }} upon deletion", "name", obj.GetName())
{{- end }}

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}
`
)


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_suitetest.go
================================================
/*
Copyright 2022 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 webhooks

import (
	"fmt"
	log "log/slog"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var (
	_ machinery.Template = &WebhookSuite{}
	_ machinery.Inserter = &WebhookSuite{}
)

// WebhookSuite scaffolds the file that sets up the webhook tests
type WebhookSuite struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	// todo: currently is not possible to know if an API was or not scaffolded. We can fix it when #1826 be addressed
	WireResource bool

	// K8SVersion define the k8s version used to do the scaffold
	// so that is possible retrieve the binaries
	K8SVersion string

	// BaseDirectoryRelativePath define the Path for the base directory when it is multigroup
	BaseDirectoryRelativePath string

	// Deprecated - The flag should be removed from go/v5
	// IsLegacyPath indicates if webhooks should be scaffolded under the API.
	// Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
	// This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
	IsLegacyPath bool
}

// SetTemplateDefaults implements machinery.Template
func (f *WebhookSuite) SetTemplateDefaults() error {
	if f.Path == "" {
		// Deprecated: Remove me when remove go/v4
		baseDir := "api"
		if !f.IsLegacyPath {
			baseDir = filepath.Join("internal", "webhook")
		}

		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join(baseDir, "%[group]", "%[version]", "webhook_suite_test.go")
		} else {
			f.Path = filepath.Join(baseDir, "%[version]", "webhook_suite_test.go")
		}
	}

	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	if f.IsLegacyPath {
		f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplateLegacy,
			machinery.NewMarkerFor(f.Path, importMarker),
			admissionImportAlias,
			machinery.NewMarkerFor(f.Path, addSchemeMarker),
			machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
			"%s",
			"%d",
		)
	} else {
		f.TemplateBody = fmt.Sprintf(webhookTestSuiteTemplate,
			machinery.NewMarkerFor(f.Path, importMarker),
			f.Resource.ImportAlias(),
			machinery.NewMarkerFor(f.Path, addSchemeMarker),
			machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
			"%s",
			"%d",
		)
	}

	if f.IsLegacyPath {
		// If is multigroup the path needs to be ../../../ since it has the group dir.
		f.BaseDirectoryRelativePath = `"..", ".."`
		if f.MultiGroup && f.Resource.Group != "" {
			f.BaseDirectoryRelativePath = `"..", "..", ".."`
		}
	} else {
		// If is multigroup the path needs to be ../../../../ since it has the group dir.
		f.BaseDirectoryRelativePath = `"..", "..", ".."`
		if f.MultiGroup && f.Resource.Group != "" {
			f.BaseDirectoryRelativePath = `"..", "..", "..", ".."`
		}
	}

	return nil
}

const (
	admissionImportAlias    = "admissionv1"
	admissionPath           = "k8s.io/api/admission/v1"
	importMarker            = "imports"
	addWebhookManagerMarker = "webhook"
	addSchemeMarker         = "scheme"
)

// GetMarkers implements file.Inserter
func (f *WebhookSuite) GetMarkers() []machinery.Marker {
	return []machinery.Marker{
		machinery.NewMarkerFor(f.Path, importMarker),
		machinery.NewMarkerFor(f.Path, addSchemeMarker),
		machinery.NewMarkerFor(f.Path, addWebhookManagerMarker),
	}
}

const (
	apiImportCodeFragment = `%s "%s"
`

	// Deprecated - TODO: remove for go/v5
	// addWebhookManagerCodeFragmentLegacy is for the path under API
	addWebhookManagerCodeFragmentLegacy = `err = (&%s{}).SetupWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

`

	addWebhookManagerCodeFragment = `err = Setup%sWebhookWithManager(mgr)
Expect(err).NotTo(HaveOccurred())

`
)

// GetCodeFragments implements file.Inserter
func (f *WebhookSuite) GetCodeFragments() machinery.CodeFragmentsMap {
	fragments := make(machinery.CodeFragmentsMap, 3)

	// Generate import code fragments
	imports := make([]string, 0)

	// Generate add scheme code fragments
	addScheme := make([]string, 0)

	// Generate add webhookManager code fragments
	addWebhookManager := make([]string, 0)
	if f.IsLegacyPath {
		imports = append(imports, fmt.Sprintf(apiImportCodeFragment, admissionImportAlias, admissionPath))
		addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragmentLegacy, f.Resource.Kind))
	} else {
		imports = append(imports, fmt.Sprintf(apiImportCodeFragment, f.Resource.ImportAlias(), f.Resource.Path))
		addWebhookManager = append(addWebhookManager, fmt.Sprintf(addWebhookManagerCodeFragment, f.Resource.Kind))
	}

	// Only store code fragments in the map if the slices are non-empty
	if len(addWebhookManager) != 0 {
		fragments[machinery.NewMarkerFor(f.Path, addWebhookManagerMarker)] = addWebhookManager
	}
	if len(imports) != 0 {
		fragments[machinery.NewMarkerFor(f.Path, importMarker)] = imports
	}
	if len(addScheme) != 0 {
		fragments[machinery.NewMarkerFor(f.Path, addSchemeMarker)] = addScheme
	}

	return fragments
}

const webhookTestSuiteTemplate = `{{ .Boilerplate }}

package {{ .Resource.Version }}

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	%s
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx context.Context
	cancel context.CancelFunc
	k8sClient client.Client
	cfg *rest.Config
	testEnv *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = %s.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	%s

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "crd", "bases")},
		ErrorIfCRDPathMissing: {{ .WireResource }},

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme:             scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:               webhookInstallOptions.LocalServingHost,
			Port:               webhookInstallOptions.LocalServingPort,
			CertDir:            webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection:     false,
		Metrics:            metricsserver.Options{BindAddress: "0"},

	})
	Expect(err).NotTo(HaveOccurred())

	%s

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%s", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close();
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are 
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join({{ .BaseDirectoryRelativePath }}, "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}
`

const webhookTestSuiteTemplateLegacy = `{{ .Boilerplate }}

package {{ .Resource.Version }}

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"path/filepath"
	"testing"
	"time"
	"runtime"

    . "github.com/onsi/ginkgo/v2"
    . "github.com/onsi/gomega"
	%s
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	apimachineryruntime "k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	cancel context.CancelFunc
	cfg *rest.Config
	ctx context.Context
	k8sClient client.Client
	testEnv *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "crd", "bases")},
		ErrorIfCRDPathMissing: {{ .WireResource }},

		// The BinaryAssetsDirectory is only required if you want to run the tests directly
		// without call the makefile target test. If not informed it will look for the
		// default path defined in controller-runtime which is /usr/local/kubebuilder/.
		// Note that you must have the required binaries setup under the bin directory to perform
		// the tests directly. When we run make test it will be setup and used automatically.
		BinaryAssetsDirectory: filepath.Join({{ .BaseDirectoryRelativePath }}, "bin", "k8s",
			fmt.Sprintf("{{ .K8SVersion }}-%%s-%%s", runtime.GOOS, runtime.GOARCH)),

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join({{ .BaseDirectoryRelativePath }}, "config", "webhook")},
		},
	}

	var err error
	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	scheme := apimachineryruntime.NewScheme()
	err = AddToScheme(scheme)
	Expect(err).NotTo(HaveOccurred())

	err = %s.AddToScheme(scheme)
	Expect(err).NotTo(HaveOccurred())

	%s

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme:             scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:               webhookInstallOptions.LocalServingHost,
			Port:               webhookInstallOptions.LocalServingPort,
			CertDir:            webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection:     false,
		Metrics:            metricsserver.Options{BindAddress: "0"},

	})
	Expect(err).NotTo(HaveOccurred())

	%s

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%s", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close();
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_template.go
================================================
/*
Copyright 2022 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 webhooks

import (
	"fmt"
	log "log/slog"
	"path/filepath"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &WebhookTest{}

// WebhookTest scaffolds the file that sets up the webhook unit tests
type WebhookTest struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin
	machinery.IfNotExistsActionMixin

	Force bool

	// Deprecated - The flag should be removed from go/v5
	// IsLegacyPath indicates if webhooks should be scaffolded under the API.
	// Webhooks are now decoupled from APIs based on controller-runtime updates and community feedback.
	// This flag ensures backward compatibility by allowing scaffolding in the legacy/deprecated path.
	IsLegacyPath bool
}

// SetTemplateDefaults implements machinery.Template
func (f *WebhookTest) SetTemplateDefaults() error {
	if f.Path == "" {
		// Deprecated: Remove me when remove go/v4
		const baseDir = "api"
		pathAPI := baseDir
		if !f.IsLegacyPath {
			pathAPI = filepath.Join("internal", "webhook")
		}

		if f.MultiGroup && f.Resource.Group != "" {
			f.Path = filepath.Join(pathAPI, "%[group]", "%[version]", "%[kind]_webhook_test.go")
		} else {
			f.Path = filepath.Join(pathAPI, "%[version]", "%[kind]_webhook_test.go")
		}
	}
	f.Path = f.Resource.Replacer().Replace(f.Path)
	log.Info(f.Path)

	webhookTestTemplate := webhookTestTemplate
	templates := make([]string, 0)
	if f.Resource.HasDefaultingWebhook() {
		templates = append(templates, defaultWebhookTestTemplate)
	}
	if f.Resource.HasValidationWebhook() {
		templates = append(templates, validateWebhookTestTemplate)
	}
	if f.Resource.HasConversionWebhook() {
		templates = append(templates, conversionWebhookTestTemplate)
	}
	f.TemplateBody = fmt.Sprintf(webhookTestTemplate, strings.Join(templates, "\n"))

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	}
	f.IfNotExistsAction = machinery.IgnoreFile

	return nil
}

const webhookTestTemplate = `{{ .Boilerplate }}

package {{ .Resource.Version }}

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	{{ if not .IsLegacyPath -}}
	{{ if not (isEmptyStr .Resource.Path) -}}
	{{ .Resource.ImportAlias }} "{{ .Resource.Path }}"
	{{- end }}
	{{- end }}
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("{{ .Resource.Kind }} Webhook", func() {
	var (
		{{- if .IsLegacyPath -}}
		obj *{{ .Resource.Kind }}
		{{- else }}
		obj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}
		oldObj *{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}
		{{- if .Resource.HasValidationWebhook }}
		validator {{ .Resource.Kind }}CustomValidator
		{{- end }}
		{{- if .Resource.HasDefaultingWebhook }}
		defaulter {{ .Resource.Kind }}CustomDefaulter
		{{- end }}
		{{- end }}
	)

	BeforeEach(func() {
		{{- if .IsLegacyPath -}}
		obj = &{{ .Resource.Kind }}{}
		{{- else }}
		obj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
		oldObj = &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
		{{- if .Resource.HasValidationWebhook }}
		validator = {{ .Resource.Kind }}CustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
		{{- end }}
		{{- if .Resource.HasDefaultingWebhook }}
		defaulter = {{ .Resource.Kind }}CustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		{{- end }}
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		{{- end }}
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	%s
})
`

const conversionWebhookTestTemplate = `
Context("When creating {{ .Resource.Kind }} under Conversion Webhook", func() {
	// TODO (user): Add logic to convert the object to the desired version and verify the conversion
	// Example:
	// It("Should convert the object correctly", func() {
	{{- if .IsLegacyPath -}}
	//     convertedObj := &{{ .Resource.Kind }}{}
	{{- else }}
	//     convertedObj := &{{ .Resource.ImportAlias }}.{{ .Resource.Kind }}{}
	{{- end }}
	//     Expect(obj.ConvertTo(convertedObj)).To(Succeed())
	//     Expect(convertedObj).ToNot(BeNil())
	// })
})
`

const validateWebhookTestTemplate = `
Context("When creating or updating {{ .Resource.Kind }} under Validating Webhook", func() {
	// TODO (user): Add logic for validating webhooks
	// Example:
	// It("Should deny creation if a required field is missing", func() {
	//     By("simulating an invalid creation scenario")
	//     obj.SomeRequiredField = ""
	{{- if .IsLegacyPath -}}
	//     Expect(obj.ValidateCreate(ctx)).Error().To(HaveOccurred())
	{{- else }}
	//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
	{{- end }}
	// })
	//
	// It("Should admit creation if all required fields are present", func() {
	//     By("simulating an invalid creation scenario")
	//     obj.SomeRequiredField = "valid_value"
	{{- if .IsLegacyPath -}}
	//     Expect(obj.ValidateCreate(ctx)).To(BeNil())
	{{- else }}
	//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
	{{- end }}
	// })
	//
	// It("Should validate updates correctly", func() {
	//     By("simulating a valid update scenario")
	{{- if .IsLegacyPath -}}
	//     oldObj := &Captain{SomeRequiredField: "valid_value"}
	//     obj.SomeRequiredField = "updated_value"
	//     Expect(obj.ValidateUpdate(ctx, oldObj)).To(BeNil())
	{{- else }}
	//     oldObj.SomeRequiredField = "updated_value"
	//     obj.SomeRequiredField = "updated_value"
	//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
	{{- end }}
	// })
})
`

const defaultWebhookTestTemplate = `
Context("When creating {{ .Resource.Kind }} under Defaulting Webhook", func() {
	// TODO (user): Add logic for defaulting webhooks
	// Example:
	// It("Should apply defaults when a required field is empty", func() {
	//     By("simulating a scenario where defaults should be applied")
	{{- if .IsLegacyPath -}}
	//     obj.SomeFieldWithDefault = ""
	//     Expect(obj.Default(ctx)).To(Succeed())
	//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
	{{- else }}
	//     obj.SomeFieldWithDefault = ""
	//     By("calling the Default method to apply defaults")
	//     defaulter.Default(ctx, obj)
	//     By("checking that the default values are set")
	//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
	{{- end }}
	// })
})
`


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_test_updater.go
================================================
/*
Copyright 2026 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 webhooks

import (
	"bytes"
	"fmt"
	log "log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

const (
	// testClosingLine is the closing line of a Describe block in test files
	testClosingLine = "\n})\n"
)

var _ machinery.Template = &WebhookTestUpdater{}

// WebhookTestUpdater updates an existing webhook test file to add validator/defaulter variables
type WebhookTestUpdater struct {
	machinery.TemplateMixin
	machinery.MultiGroupMixin
	machinery.ResourceMixin
}

// GetPath implements file.Builder
func (f *WebhookTestUpdater) GetPath() string {
	baseDir := filepath.Join("internal", "webhook")

	var path string
	if f.MultiGroup && f.Resource.Group != "" {
		path = filepath.Join(baseDir, "%[group]", "%[version]", "%[kind]_webhook_test.go")
	} else {
		path = filepath.Join(baseDir, "%[version]", "%[kind]_webhook_test.go")
	}

	return f.Resource.Replacer().Replace(path)
}

// GetIfExistsAction implements file.Builder
func (*WebhookTestUpdater) GetIfExistsAction() machinery.IfExistsAction {
	return machinery.OverwriteFile
}

// SetTemplateDefaults implements file.Template
func (f *WebhookTestUpdater) SetTemplateDefaults() error {
	filePath := f.GetPath()

	// Read the existing file
	content, err := os.ReadFile(filePath)
	if err != nil {
		log.Warn("Unable to read webhook test file, skipping update", "file", filePath, "error", err)
		// Return nil to continue - file might not exist yet
		return nil
	}

	fileContent := string(content)
	modified := false

	// Check if we need to add validator variable and tests
	validatorVar := fmt.Sprintf("validator %sCustomValidator", f.Resource.Kind)
	if f.Resource.HasValidationWebhook() && !bytes.Contains(content, []byte(validatorVar)) {
		fileContent = f.addValidatorVariable(fileContent)
		fileContent = f.addValidatorInit(fileContent)
		fileContent = f.addValidationTestContext(fileContent)
		modified = true
	}

	// Check if we need to add defaulter variable and tests
	defaulterVar := fmt.Sprintf("defaulter %sCustomDefaulter", f.Resource.Kind)
	if f.Resource.HasDefaultingWebhook() && !bytes.Contains(content, []byte(defaulterVar)) {
		fileContent = f.addDefaulterVariable(fileContent)
		fileContent = f.addDefaulterInit(fileContent)
		fileContent = f.addDefaultingTestContext(fileContent)
		modified = true
	}

	// Check if we need to add conversion test context
	if f.Resource.HasConversionWebhook() && !bytes.Contains(content, []byte("Conversion Webhook")) {
		fileContent = f.addConversionTestContext(fileContent)
		modified = true
	}

	if !modified {
		// No updates needed, skip writing
		return nil
	}

	f.TemplateBody = fileContent
	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

// addValidatorVariable adds the validator variable to the var block
func (f *WebhookTestUpdater) addValidatorVariable(content string) string {
	varName := "validator"
	typeName := f.Resource.Kind + "CustomValidator"
	return f.addVariableToBlock(content, varName, typeName)
}

// addDefaulterVariable adds the defaulter variable to the var block
func (f *WebhookTestUpdater) addDefaulterVariable(content string) string {
	varName := "defaulter"
	typeName := f.Resource.Kind + "CustomDefaulter"
	return f.addVariableToBlock(content, varName, typeName)
}

// addVariableToBlock adds a variable declaration to the var block before the closing paren
func (f *WebhookTestUpdater) addVariableToBlock(content, varName, typeName string) string {
	varBlockPattern := regexp.MustCompile(`(?s)(var\s*\(\s*)([^)]*?)(\s*\))`)

	if match := varBlockPattern.FindStringSubmatch(content); len(match) > 3 {
		opening := match[1]
		declarations := match[2]
		closing := match[3]

		indent := f.detectIndentationInBlock(declarations)
		if indent == "" {
			indent = "\t\t"
		}

		varDecl := fmt.Sprintf("\n%s%s %s", indent, varName, typeName)
		newVarBlock := opening + declarations + varDecl + closing

		return strings.Replace(content, match[0], newVarBlock, 1)
	}

	log.Warn("Could not find var block in test file",
		"kind", f.Resource.Kind,
		"variable", varName,
		"suggestion", fmt.Sprintf("Manually add '%s %s' to the var block", varName, typeName))
	return content
}

// detectIndentationInBlock extracts indentation from existing code
func (f *WebhookTestUpdater) detectIndentationInBlock(blockContent string) string {
	lines := strings.Split(blockContent, "\n")
	for i := len(lines) - 1; i >= 0; i-- {
		line := lines[i]
		trimmed := strings.TrimSpace(line)
		if trimmed != "" && trimmed != "var" && trimmed != "(" {
			trimmedLeft := strings.TrimLeft(line, " \t")
			if len(line) > len(trimmedLeft) {
				return line[:len(line)-len(trimmedLeft)]
			}
		}
	}
	return "\t\t"
}

// addValidatorInit adds validator initialization in BeforeEach
func (f *WebhookTestUpdater) addValidatorInit(content string) string {
	varName := "validator"
	typeName := f.Resource.Kind + "CustomValidator"
	return f.addWebhookInit(content, varName, typeName)
}

// addDefaulterInit adds defaulter initialization in BeforeEach
func (f *WebhookTestUpdater) addDefaulterInit(content string) string {
	varName := "defaulter"
	typeName := f.Resource.Kind + "CustomDefaulter"
	return f.addWebhookInit(content, varName, typeName)
}

// addWebhookInit adds webhook variable initialization at the end of BeforeEach block
func (f *WebhookTestUpdater) addWebhookInit(content, varName, typeName string) string {
	checkPattern := fmt.Sprintf("%s = %s", varName, typeName)
	if strings.Contains(content, checkPattern) {
		return content
	}

	// Add at the END of BeforeEach block (before closing brace)
	beforeEachPattern := regexp.MustCompile(`(?s)(BeforeEach\s*\(\s*func\s*\(\s*\)\s*\{)(.*?)(\n\s*\}\s*\))`)

	if match := beforeEachPattern.FindStringSubmatch(content); len(match) > 3 {
		opening := match[1]
		blockContent := match[2]
		closing := match[3]

		closingLines := strings.Split(closing, "\n")
		indent := "\t\t"
		if len(closingLines) > 1 {
			closingLine := closingLines[1]
			baseIndent := closingLine[:len(closingLine)-len(strings.TrimLeft(closingLine, " \t"))]
			indent = baseIndent + "\t"
		}

		init := fmt.Sprintf("\n%s%s = %s{}\n%sExpect(%s).NotTo(BeNil(), \"Expected %s to be initialized\")",
			indent, varName, typeName, indent, varName, varName)
		replacement := opening + blockContent + init + closing
		return strings.Replace(content, match[0], replacement, 1)
	}

	log.Warn("Could not find BeforeEach block",
		"kind", f.Resource.Kind,
		"variable", varName,
		"suggestion", fmt.Sprintf("Manually add '%s = %s{}' to BeforeEach", varName, typeName))
	return content
}

// addValidationTestContext adds the validation test context
func (f *WebhookTestUpdater) addValidationTestContext(content string) string {
	testContext := fmt.Sprintf(`
	Context("When creating or updating %s under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})
`, f.Resource.Kind)

	return f.addContextToEnd(content, testContext)
}

// addDefaultingTestContext adds the defaulting test context
func (f *WebhookTestUpdater) addDefaultingTestContext(content string) string {
	testContext := fmt.Sprintf(`
	Context("When creating %s under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})
`, f.Resource.Kind)

	return f.addContextToEnd(content, testContext)
}

// addConversionTestContext adds the conversion test context
func (f *WebhookTestUpdater) addConversionTestContext(content string) string {
	testContext := fmt.Sprintf(`
	Context("When creating %s under Conversion Webhook", func() {
		// TODO (user): Add logic to convert the object to the desired version and verify the conversion
		// Example:
		// It("Should convert the object correctly", func() {
		//     convertedObj := &%s.%s{}
		//     Expect(obj.ConvertTo(convertedObj)).To(Succeed())
		//     Expect(convertedObj).ToNot(BeNil())
		// })
	})
`, f.Resource.Kind, f.Resource.ImportAlias(), f.Resource.Kind)

	return f.addContextToEnd(content, testContext)
}

// addContextToEnd adds a test context before the Describe block's closing })
func (f *WebhookTestUpdater) addContextToEnd(content, testContext string) string {
	// Find the Describe block's closing })
	describePattern := regexp.MustCompile(`(?s)var\s*_\s*=\s*Describe\([^}]+`)
	if match := describePattern.FindStringIndex(content); match != nil {
		lastClosing := regexp.MustCompile(`\n\}\)\s*$`)
		if closingMatch := lastClosing.FindStringIndex(content); closingMatch != nil {
			return content[:closingMatch[0]] + testContext + content[closingMatch[0]:]
		}
	}

	// Fallback: find last }) in file (test files typically end with })
	if idx := strings.LastIndex(content, testClosingLine); idx != -1 {
		return content[:idx] + testContext + testClosingLine
	}

	// Last resort: append at end of file
	content = strings.TrimRight(content, "\n")
	return content + testContext + "\n"
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks/webhook_updater.go
================================================
/*
Copyright 2026 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 webhooks

import (
	"bytes"
	"fmt"
	log "log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

const coreGroup = "core"

var _ machinery.Template = &WebhookUpdater{}

// WebhookUpdater updates an existing webhook file to add additional webhook types
type WebhookUpdater struct {
	machinery.TemplateMixin
	machinery.RepositoryMixin
	machinery.MultiGroupMixin
	machinery.BoilerplateMixin
	machinery.ResourceMixin

	// QualifiedGroupWithDash is the Group domain for the Resource replacing '.' with '-'
	QualifiedGroupWithDash string

	// AdmissionReviewVersions defines value for AdmissionReviewVersions marker
	AdmissionReviewVersions string
}

// GetPath implements file.Builder
func (f *WebhookUpdater) GetPath() string {
	baseDir := filepath.Join("internal", "webhook")

	var path string
	if f.MultiGroup && f.Resource.Group != "" {
		path = filepath.Join(baseDir, "%[group]", "%[version]", "%[kind]_webhook.go")
	} else {
		path = filepath.Join(baseDir, "%[version]", "%[kind]_webhook.go")
	}

	return f.Resource.Replacer().Replace(path)
}

// GetIfExistsAction implements file.Builder
func (*WebhookUpdater) GetIfExistsAction() machinery.IfExistsAction {
	return machinery.OverwriteFile
}

// SetTemplateDefaults implements file.Template
func (f *WebhookUpdater) SetTemplateDefaults() error {
	filePath := f.GetPath()

	// Read the existing file
	content, err := os.ReadFile(filePath)
	if err != nil {
		log.Error("failed to read webhook file", "file", filePath, "error", err)
		return fmt.Errorf("failed to read webhook file: %w", err)
	}

	f.QualifiedGroupWithDash = strings.ReplaceAll(f.Resource.QualifiedGroup(), ".", "-")
	f.AdmissionReviewVersions = "v1"

	fileContent := string(content)
	var newCode strings.Builder

	// Add defaulting webhook if requested and not already present
	defaulterType := fmt.Sprintf("%sCustomDefaulter", f.Resource.Kind)
	if f.Resource.HasDefaultingWebhook() {
		typeDefPattern := regexp.MustCompile(fmt.Sprintf(`type\s+%s\s+struct`, defaulterType))
		if typeDefPattern.MatchString(string(content)) {
			log.Info("Defaulting webhook already exists, skipping", "kind", f.Resource.Kind)
		} else {
			defaultingCode := f.generateDefaultingWebhookCode()
			if defaultingCode != "" {
				newCode.WriteString(defaultingCode)
			}

			setupCode := f.generateDefaulterSetupCode()
			if !strings.Contains(fileContent, fmt.Sprintf("WithDefaulter(&%s{})", defaulterType)) {
				fileContent = f.injectBeforeComplete(fileContent, setupCode)
			}
		}
	}

	// Add validation webhook if requested and not already present
	validatorType := fmt.Sprintf("%sCustomValidator", f.Resource.Kind)
	if f.Resource.HasValidationWebhook() {
		typeDefPattern := regexp.MustCompile(fmt.Sprintf(`type\s+%s\s+struct`, validatorType))
		if typeDefPattern.MatchString(string(content)) {
			log.Info("Validation webhook already exists, skipping", "kind", f.Resource.Kind)
		} else {
			if !bytes.Contains(content, []byte("sigs.k8s.io/controller-runtime/pkg/webhook/admission")) {
				fileContent = f.addAdmissionImport(fileContent)
			}

			validationCode := f.generateValidationWebhookCode()
			if validationCode != "" {
				newCode.WriteString(validationCode)
			}

			setupCode := f.generateValidatorSetupCode()
			if !strings.Contains(fileContent, fmt.Sprintf("WithValidator(&%s{})", validatorType)) {
				fileContent = f.injectBeforeComplete(fileContent, setupCode)
			}
		}
	}

	// Append new webhook code at the end of the file
	if newCode.Len() > 0 {
		fileContent = strings.TrimRight(fileContent, "\n") + "\n" + newCode.String()
	}

	f.TemplateBody = fileContent
	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

// injectBeforeComplete injects webhook setup code before the Complete() call
func (f *WebhookUpdater) injectBeforeComplete(content, code string) string {
	completePattern := regexp.MustCompile(`(?m)^(\s*)(?:\.)?\s*Complete\(\s*\)`)

	if match := completePattern.FindStringSubmatch(content); len(match) > 1 {
		completeCall := match[0]
		baseIndent := match[1]

		beforeComplete := content[:strings.Index(content, completeCall)]
		indent := f.detectIndentationBeforeComplete(beforeComplete, baseIndent)
		adjustedCode := f.adjustCodeIndentation(code, indent)

		insertPos := strings.Index(content, completeCall)
		return content[:insertPos] + adjustedCode + content[insertPos:]
	}

	log.Warn("Could not find Complete() call in setup function",
		"kind", f.Resource.Kind,
		"suggestion", "Manually wire webhook in SetupWebhookWithManager")
	return content
}

// detectIndentationBeforeComplete extracts indentation from method chain lines
func (f *WebhookUpdater) detectIndentationBeforeComplete(beforeComplete, baseIndent string) string {
	lines := strings.Split(beforeComplete, "\n")

	for i := len(lines) - 1; i >= 0 && i >= len(lines)-5; i-- {
		line := lines[i]
		trimmed := strings.TrimSpace(line)

		if strings.HasPrefix(trimmed, ".") && (strings.Contains(trimmed, "For(") ||
			strings.Contains(trimmed, "With") || strings.Contains(trimmed, "Owns(")) {
			leadingSpace := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
			if leadingSpace != "" {
				return leadingSpace
			}
		}
	}

	if strings.Contains(baseIndent, "\t") {
		return baseIndent + "\t\t"
	}
	return baseIndent + "        "
}

// adjustCodeIndentation replaces existing indentation with target indentation
func (f *WebhookUpdater) adjustCodeIndentation(code, targetIndent string) string {
	lines := strings.Split(code, "\n")
	adjusted := make([]string, len(lines))

	for i, line := range lines {
		if strings.TrimSpace(line) == "" {
			adjusted[i] = line
			continue
		}

		trimmed := strings.TrimLeft(line, " \t")
		if len(trimmed) < len(line) {
			adjusted[i] = targetIndent + trimmed
		} else {
			adjusted[i] = line
		}
	}

	return strings.Join(adjusted, "\n")
}

// addAdmissionImport adds the admission package import after the webhook import
func (f *WebhookUpdater) addAdmissionImport(content string) string {
	admissionImport := "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
	if strings.Contains(content, admissionImport) {
		return content
	}

	// Add after webhook import
	webhookPattern := regexp.MustCompile(`(?m)^(\s*)"sigs\.k8s\.io/controller-runtime/pkg/webhook"`)

	if match := webhookPattern.FindStringSubmatch(content); len(match) > 1 {
		indent := match[1]
		webhookLine := match[0]
		replacement := webhookLine + "\n" + indent + `"` + admissionImport + `"`
		return strings.Replace(content, webhookLine, replacement, 1)
	}

	// Fallback: add to end of import block
	importBlockPattern := regexp.MustCompile(`(?s)(import\s*\([^)]+)(\s*\))`)

	if match := importBlockPattern.FindStringSubmatch(content); len(match) > 2 {
		lastImportPattern := regexp.MustCompile(`(?m)^(\s*)"[^"]+"\s*$`)
		imports := lastImportPattern.FindAllStringSubmatch(match[1], -1)

		indent := "\t"
		if len(imports) > 0 && len(imports[len(imports)-1]) > 1 {
			indent = imports[len(imports)-1][1]
		}

		newImport := "\n" + indent + `"` + admissionImport + `"`
		replacement := match[1] + newImport + match[2]
		return strings.Replace(content, match[0], replacement, 1)
	}

	log.Warn("Could not add admission import",
		"kind", f.Resource.Kind,
		"suggestion", "Manually add: "+admissionImport)
	return content
}

// generateDefaulterSetupCode generates the setup code for defaulting webhook
func (f *WebhookUpdater) generateDefaulterSetupCode() string {
	code := fmt.Sprintf("\t\tWithDefaulter(&%sCustomDefaulter{}).", f.Resource.Kind)
	if f.Resource.Webhooks.DefaultingPath != "" {
		code += fmt.Sprintf("\n\t\tWithDefaulterCustomPath(\"%s\").", f.Resource.Webhooks.DefaultingPath)
	}
	return code + "\n"
}

// generateValidatorSetupCode generates the setup code for validation webhook
func (f *WebhookUpdater) generateValidatorSetupCode() string {
	code := fmt.Sprintf("\t\tWithValidator(&%sCustomValidator{}).", f.Resource.Kind)
	if f.Resource.Webhooks.ValidationPath != "" {
		code += fmt.Sprintf("\n\t\tWithValidatorCustomPath(\"%s\").", f.Resource.Webhooks.ValidationPath)
	}
	return code + "\n"
}

// generateDefaultingWebhookCode generates the defaulting webhook code
func (f *WebhookUpdater) generateDefaultingWebhookCode() string {
	var code strings.Builder

	// Webhook marker
	defaultingPath := f.Resource.Webhooks.DefaultingPath
	if defaultingPath == "" {
		if f.Resource.Core && f.Resource.QualifiedGroup() == coreGroup {
			defaultingPath = fmt.Sprintf("/mutate--%s-%s",
				f.Resource.Version, strings.ToLower(f.Resource.Kind))
		} else {
			defaultingPath = fmt.Sprintf("/mutate-%s-%s-%s",
				f.QualifiedGroupWithDash, f.Resource.Version, strings.ToLower(f.Resource.Kind))
		}
	}

	//nolint:lll
	code.WriteString(fmt.Sprintf(
		`
// +kubebuilder:webhook:path=%s,mutating=true,failurePolicy=fail,sideEffects=None,groups=%s,resources=%s,verbs=create;update,versions=%s,name=m%s-%s.kb.io,admissionReviewVersions=%s

// %sCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind %s when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type %sCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

`,
		defaultingPath, f.getGroupValue(), f.Resource.Plural, f.Resource.Version,
		strings.ToLower(f.Resource.Kind), f.Resource.Version,
		f.AdmissionReviewVersions,
		f.Resource.Kind, f.Resource.Kind, f.Resource.Kind))

	// Default method
	objType := f.Resource.ImportAlias() + "." + f.Resource.Kind

	code.WriteString(fmt.Sprintf(
		`// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind %s.
func (d *%sCustomDefaulter) Default(_ context.Context, obj *%s) error {
	%slog.Info("Defaulting for %s", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}
`,
		f.Resource.Kind, f.Resource.Kind, objType,
		strings.ToLower(f.Resource.Kind), f.Resource.Kind))

	return code.String()
}

// generateValidationWebhookCode generates the validation webhook code
func (f *WebhookUpdater) generateValidationWebhookCode() string {
	var code strings.Builder

	// Webhook marker
	validationPath := f.Resource.Webhooks.ValidationPath
	if validationPath == "" {
		if f.Resource.Core && f.Resource.QualifiedGroup() == coreGroup {
			validationPath = fmt.Sprintf("/validate--%s-%s",
				f.Resource.Version, strings.ToLower(f.Resource.Kind))
		} else {
			validationPath = fmt.Sprintf("/validate-%s-%s-%s",
				f.QualifiedGroupWithDash, f.Resource.Version, strings.ToLower(f.Resource.Kind))
		}
	}

	code.WriteString(
		`// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
`)
	//nolint:lll
	code.WriteString(fmt.Sprintf(
		`// +kubebuilder:webhook:path=%s,mutating=false,failurePolicy=fail,sideEffects=None,groups=%s,resources=%s,verbs=create;update,versions=%s,name=v%s-%s.kb.io,admissionReviewVersions=%s

// %sCustomValidator struct is responsible for validating the %s resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type %sCustomValidator struct{
	// TODO(user): Add more fields as needed for validation
}

`,
		validationPath, f.getGroupValue(), f.Resource.Plural, f.Resource.Version,
		strings.ToLower(f.Resource.Kind), f.Resource.Version, f.AdmissionReviewVersions,
		f.Resource.Kind, f.Resource.Kind, f.Resource.Kind))

	// Validation methods
	objType := f.Resource.ImportAlias() + "." + f.Resource.Kind

	code.WriteString(fmt.Sprintf(
		`// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type %s.
func (v *%sCustomValidator) ValidateCreate(_ context.Context, obj *%s) (admission.Warnings, error) {
	%slog.Info("Validation for %s upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type %s.
func (v *%sCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *%s) (admission.Warnings, error) {
	%slog.Info("Validation for %s upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type %s.
func (v *%sCustomValidator) ValidateDelete(_ context.Context, obj *%s) (admission.Warnings, error) {
	%slog.Info("Validation for %s upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}
`,
		f.Resource.Kind, f.Resource.Kind, objType,
		strings.ToLower(f.Resource.Kind), f.Resource.Kind,
		f.Resource.Kind, f.Resource.Kind, objType,
		strings.ToLower(f.Resource.Kind), f.Resource.Kind,
		f.Resource.Kind, f.Resource.Kind, objType,
		strings.ToLower(f.Resource.Kind), f.Resource.Kind))

	return code.String()
}

// getGroupValue returns the group value for webhook markers
func (f *WebhookUpdater) getGroupValue() string {
	if f.Resource.Core && f.Resource.QualifiedGroup() == coreGroup {
		return `""`
	}
	return f.Resource.QualifiedGroup()
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/suite_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestScaffolds(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Scaffolds Integration Suite")
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/webhook.go
================================================
/*
Copyright 2022 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 scaffolds

import (
	"errors"
	"fmt"
	log "log/slog"
	"strings"

	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
	pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/api"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/cmd"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/hack"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/test/e2e"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds/internal/templates/webhooks"
)

var _ plugins.Scaffolder = &webhookScaffolder{}

type webhookScaffolder struct {
	config   config.Config
	resource resource.Resource

	// fs is the filesystem that will be used by the scaffolder
	fs machinery.Filesystem

	// force indicates whether to scaffold controller files even if it exists or not
	force bool

	// Deprecated - TODO: remove it for go/v5
	// isLegacy indicates that the resource should be created in the legacy path under the api
	isLegacy bool
}

// NewWebhookScaffolder returns a new Scaffolder for v2 webhook creation operations
func NewWebhookScaffolder(cfg config.Config, res resource.Resource, force bool, isLegacy bool) plugins.Scaffolder {
	return &webhookScaffolder{
		config:   cfg,
		resource: res,
		force:    force,
		isLegacy: isLegacy,
	}
}

// InjectFS implements cmdutil.Scaffolder
func (s *webhookScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// Scaffold implements cmdutil.Scaffolder
func (s *webhookScaffolder) Scaffold() error {
	log.Info("Writing scaffold for you to edit...")

	// Load the boilerplate
	boilerplate, err := afero.ReadFile(s.fs.FS, hack.DefaultBoilerplatePath)
	if err != nil {
		if errors.Is(err, afero.ErrFileNotFound) {
			log.Warn("unable to find boilerplate file. "+
				"This file is used to generate the license header in the project..\n"+
				"Note that controller-gen will also use this. Ensure that you "+
				"add the license file or configure your project accordingly",
				"file_path", hack.DefaultBoilerplatePath, "error", err)
			boilerplate = []byte("")
		} else {
			return fmt.Errorf("error scaffolding webhook: failed to load boilerplate: %w", err)
		}
	}

	// Initialize the machinery.Scaffold that will write the files to disk
	scaffold := machinery.NewScaffold(s.fs,
		machinery.WithConfig(s.config),
		machinery.WithBoilerplate(string(boilerplate)),
		machinery.WithResource(&s.resource),
	)

	// Keep track of these values before the update
	doDefaulting := s.resource.HasDefaultingWebhook()
	doValidation := s.resource.HasValidationWebhook()
	doConversion := s.resource.HasConversionWebhook()

	if err = s.config.UpdateResource(s.resource); err != nil {
		return fmt.Errorf("error updating resource: %w", err)
	}

	// Check if webhook files exist
	webhookFilePath := s.getWebhookFilePath()
	webhookFileExists := false
	if _, statErr := s.fs.FS.Stat(webhookFilePath); statErr == nil {
		webhookFileExists = true
	}

	webhookTestFilePath := s.getWebhookTestFilePath()
	webhookTestFileExists := false
	if _, statErr := s.fs.FS.Stat(webhookTestFilePath); statErr == nil {
		webhookTestFileExists = true
	}

	// Scaffold or update webhook file (for all webhook types)
	// Note: Conversion webhooks also need a webhook.go file with minimal setup (.For(&Type{}).Complete())
	// This is how controller-runtime discovers Hub/Convertible interfaces
	if doDefaulting || doValidation || doConversion {
		if err = s.scaffoldWebhookFile(scaffold, webhookFileExists); err != nil {
			return err
		}

		// Update main.go to wire webhook setup function (for all webhook types)
		if err = scaffold.Execute(
			&cmd.MainUpdater{WireWebhook: true, IsLegacyPath: s.isLegacy},
		); err != nil {
			return fmt.Errorf("error updating main.go: %w", err)
		}
	}

	// Scaffold or update webhook test file (for all webhook types)
	if err = s.scaffoldWebhookTestFile(scaffold, webhookTestFileExists); err != nil {
		return err
	}

	// Update e2e tests
	// WireWebhook controls webhook service readiness checks (for defaulting/validation)
	// But conversion webhooks still need CA injection tests (handled inside updater)
	if err = scaffold.Execute(
		&e2e.WebhookTestUpdater{WireWebhook: doDefaulting || doValidation},
	); err != nil {
		return fmt.Errorf("error updating e2e tests: %w", err)
	}

	if doConversion {
		// Update the types file to add storage version marker
		if err = scaffold.Execute(&api.TypesUpdater{}); err != nil {
			return fmt.Errorf("error updating types file with storage version marker: %w", err)
		}

		if err = scaffold.Execute(&api.Hub{Force: s.force}); err != nil {
			return fmt.Errorf("error scaffold resource with hub: %w", err)
		}

		for _, spoke := range s.resource.Webhooks.Spoke {
			log.Info("Scaffolding for spoke version", "version", spoke)
			if err = scaffold.Execute(&api.Spoke{Force: s.force, SpokeVersion: spoke}); err != nil {
				return fmt.Errorf("failed to scaffold spoke %s: %w", spoke, err)
			}
		}

		log.Info(`Webhook server has been set up for you.
You need to implement the conversion.Hub and conversion.Convertible interfaces for your CRD types.`)
	}

	// Scaffold webhook suite test for all webhook types
	// Note: Conversion webhooks also need the suite to register with envtest
	if doDefaulting || doValidation || doConversion {
		if err = scaffold.Execute(&webhooks.WebhookSuite{IsLegacyPath: s.isLegacy}); err != nil {
			return fmt.Errorf("error scaffold webhook suite: %w", err)
		}
	}

	// TODO: remove for go/v5
	if !s.isLegacy {
		if hasInternalController, err := pluginutil.HasFileContentWith("Dockerfile", "internal/controller"); err != nil {
			log.Error("failed to read Dockerfile to check if webhook(s) will be properly copied", "error", err)
		} else if hasInternalController {
			log.Warn("Dockerfile is copying internal/controller; to allow copying webhooks, " +
				"it will be edited, and `internal/controller` will be replaced by `internal/`")

			if err = pluginutil.ReplaceInFile("Dockerfile", "internal/controller", "internal/"); err != nil {
				log.Error("failed to replace \"internal/controller\" with \"internal/\" in the Dockerfile", "error", err)
			}
		}
	}
	return nil
}

// getWebhookFilePath returns the path to the webhook file
func (s *webhookScaffolder) getWebhookFilePath() string {
	baseDir := "api"
	if !s.isLegacy {
		baseDir = "internal/webhook"
	}

	var path string
	if s.config.IsMultiGroup() && s.resource.Group != "" {
		path = fmt.Sprintf("%s/%s/%s/%s_webhook.go",
			baseDir, s.resource.Group, s.resource.Version, strings.ToLower(s.resource.Kind))
	} else {
		path = fmt.Sprintf("%s/%s/%s_webhook.go",
			baseDir, s.resource.Version, strings.ToLower(s.resource.Kind))
	}

	return path
}

// getWebhookTestFilePath returns the path to the webhook test file
func (s *webhookScaffolder) getWebhookTestFilePath() string {
	baseDir := "api"
	if !s.isLegacy {
		baseDir = "internal/webhook"
	}

	var path string
	if s.config.IsMultiGroup() && s.resource.Group != "" {
		path = fmt.Sprintf("%s/%s/%s/%s_webhook_test.go",
			baseDir, s.resource.Group, s.resource.Version, strings.ToLower(s.resource.Kind))
	} else {
		path = fmt.Sprintf("%s/%s/%s_webhook_test.go",
			baseDir, s.resource.Version, strings.ToLower(s.resource.Kind))
	}

	return path
}

// scaffoldWebhookFile creates or updates the webhook implementation file
func (s *webhookScaffolder) scaffoldWebhookFile(scaffold *machinery.Scaffold, fileExists bool) error {
	if !fileExists || s.force {
		if err := scaffold.Execute(
			&webhooks.Webhook{Force: s.force, IsLegacyPath: s.isLegacy},
		); err != nil {
			return fmt.Errorf("error creating webhook: %w", err)
		}
	} else if fileExists && !s.force && !s.isLegacy {
		log.Info("Adding new webhook type to existing file")
		if err := scaffold.Execute(
			&webhooks.WebhookUpdater{},
		); err != nil {
			return fmt.Errorf("error updating webhook: %w", err)
		}
	}
	return nil
}

// scaffoldWebhookTestFile creates or updates the webhook test file
func (s *webhookScaffolder) scaffoldWebhookTestFile(scaffold *machinery.Scaffold, fileExists bool) error {
	if !fileExists || s.force {
		if err := scaffold.Execute(
			&webhooks.WebhookTest{Force: s.force, IsLegacyPath: s.isLegacy},
		); err != nil {
			return fmt.Errorf("error creating webhook test: %w", err)
		}
	} else if fileExists && !s.force && !s.isLegacy {
		if err := scaffold.Execute(
			&webhooks.WebhookTestUpdater{},
		); err != nil {
			return fmt.Errorf("error updating webhook test: %w", err)
		}
	}
	return nil
}


================================================
FILE: pkg/plugins/golang/v4/scaffolds/webhook_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

var _ = Describe("Webhook Incremental Scaffolding", func() {
	var (
		kbc *utils.TestContext
	)

	BeforeEach(func() {
		var err error
		kbc, err = utils.NewTestContext(pluginutil.KubebuilderBinName, "GO111MODULE=on")
		Expect(err).NotTo(HaveOccurred())
		Expect(kbc.Prepare()).To(Succeed())

		By("initializing a project")
		err = kbc.Init(
			"--domain", "test.io",
			"--repo", "test.io/webhooktest",
		)
		Expect(err).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		By("removing working directory")
		kbc.Destroy()
	})

	Context("When creating webhooks incrementally", func() {
		It("should support adding validation to existing defaulting webhook", func() {
			By("creating an API")
			err := kbc.CreateAPI(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestIncremental",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating defaulting webhook")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestIncremental",
				"--defaulting",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("adding validation webhook WITHOUT --force")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestIncremental",
				"--programmatic-validation",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying both webhooks are present in test file")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testincremental_webhook_test.go")
			content, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("defaulter TestIncrementalCustomDefaulter"))
			Expect(string(content)).To(ContainSubstring("validator TestIncrementalCustomValidator"))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestIncremental under Defaulting Webhook\""))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating or updating TestIncremental under Validating Webhook\""))
		})

		It("should support adding defaulting to existing validation webhook", func() {
			By("creating an API")
			err := kbc.CreateAPI(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestReverse",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating validation webhook")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestReverse",
				"--programmatic-validation",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("adding defaulting webhook WITHOUT --force")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestReverse",
				"--defaulting",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying both webhooks are present")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testreverse_webhook_test.go")
			content, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("validator TestReverseCustomValidator"))
			Expect(string(content)).To(ContainSubstring("defaulter TestReverseCustomDefaulter"))
		})

		It("should support conversion-only webhooks without defaulting/validation", func() {
			By("creating API v1")
			err := kbc.CreateAPI(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestConversion",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating API v2")
			err = kbc.CreateAPI(
				"--group", "test",
				"--version", "v2",
				"--kind", "TestConversion",
				"--resource=false", "--controller=false",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating conversion webhook")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestConversion",
				"--conversion",
				"--spoke", "v2",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying conversion test context is created")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testconversion_webhook_test.go")
			content, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestConversion under Conversion Webhook\""))
			Expect(string(content)).NotTo(ContainSubstring("defaulter"))
			Expect(string(content)).NotTo(ContainSubstring("validator"))

			By("verifying webhook file was created with minimal setup")
			webhookFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testconversion_webhook.go")
			webhookContent, err := os.ReadFile(webhookFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(webhookContent)).To(ContainSubstring("SetupTestConversionWebhookWithManager"))
			Expect(string(webhookContent)).To(ContainSubstring("NewWebhookManagedBy(mgr, &testv1.TestConversion{})"))
			Expect(string(webhookContent)).To(ContainSubstring("Complete()"))
			Expect(string(webhookContent)).NotTo(ContainSubstring("CustomDefaulter"))
			Expect(string(webhookContent)).NotTo(ContainSubstring("CustomValidator"))

			By("verifying conversion webhook IS wired in main.go")
			mainFile := filepath.Join(kbc.Dir, "cmd/main.go")
			mainContent, err := os.ReadFile(mainFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(mainContent)).To(ContainSubstring("SetupTestConversionWebhookWithManager"))

			By("verifying e2e test has conversion CA injection check")
			e2eFile := filepath.Join(kbc.Dir, "test/e2e/e2e_test.go")
			e2eContent, err := os.ReadFile(e2eFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(e2eContent)).To(ContainSubstring("CA injection for TestConversion conversion webhook"))

			By("verifying webhook suite test was created")
			suiteFile := filepath.Join(kbc.Dir, "internal/webhook/v1/webhook_suite_test.go")
			_, err = os.Stat(suiteFile)
			Expect(err).NotTo(HaveOccurred(), "Webhook suite test file should exist")
		})

		It("should support multiversion scenario: conversion then defaulting/validation", func() {
			By("creating API v1")
			err := kbc.CreateAPI(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestMulti",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating API v2")
			err = kbc.CreateAPI(
				"--group", "test",
				"--version", "v2",
				"--kind", "TestMulti",
				"--resource=false", "--controller=false",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating conversion webhook first")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestMulti",
				"--conversion",
				"--spoke", "v2",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("adding defaulting and validation webhooks WITHOUT --force")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestMulti",
				"--defaulting",
				"--programmatic-validation",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying all three webhook types are present")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testmulti_webhook_test.go")
			content, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("defaulter TestMultiCustomDefaulter"))
			Expect(string(content)).To(ContainSubstring("validator TestMultiCustomValidator"))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestMulti under Conversion Webhook\""))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestMulti under Defaulting Webhook\""))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating or updating TestMulti under Validating Webhook\""))

			By("verifying defaulting/validation webhooks are wired in main.go")
			mainFile := filepath.Join(kbc.Dir, "cmd/main.go")
			mainContent, err := os.ReadFile(mainFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(mainContent)).To(ContainSubstring("SetupTestMultiWebhookWithManager"))
		})
	})

	Context("When user customizes webhook files", func() {
		It("should preserve customizations when adding new webhook types", func() {
			By("creating an API")
			err := kbc.CreateAPI(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestCustom",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating defaulting webhook")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestCustom",
				"--defaulting",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("simulating user customizations to test file")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testcustom_webhook_test.go")
			testContent, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			By("customizing: renaming variables obj->myObj, oldObj->oldMyObj")
			modified := strings.ReplaceAll(string(testContent), "obj       *testv1.TestCustom", "myObj     *testv1.TestCustom")
			modified = strings.ReplaceAll(modified, "oldObj    *testv1.TestCustom", "oldMyObj  *testv1.TestCustom")
			modified = strings.ReplaceAll(modified, "obj = &testv1.TestCustom{}", "myObj = &testv1.TestCustom{}")
			modified = strings.ReplaceAll(modified, "oldObj = &testv1.TestCustom{}", "oldMyObj = &testv1.TestCustom{}")
			modified = strings.ReplaceAll(modified, "Expect(oldObj)", "Expect(oldMyObj)")
			modified = strings.ReplaceAll(modified, "Expect(obj)", "Expect(myObj)")

			By("customizing: adding custom setup code to BeforeEach")
			customCode := `		// Custom test setup
		myObj.Name = "my-test-object"
		myObj.Namespace = "test-ns"
		oldMyObj.Name = "old-object"
	})`
			modified = strings.Replace(modified, "	})\n\n	AfterEach(func() {", customCode+"\n\n	AfterEach(func() {", 1)

			err = os.WriteFile(testFile, []byte(modified), 0644)
			Expect(err).NotTo(HaveOccurred())

			By("customizing: adding custom implementation to webhook")
			webhookFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testcustom_webhook.go")
			webhookContent, err := os.ReadFile(webhookFile)
			Expect(err).NotTo(HaveOccurred())

			modifiedWebhook := strings.ReplaceAll(string(webhookContent),
				"// TODO(user): fill in your defaulting logic.",
				`// My custom defaulting logic
	if testcustom.Spec.Replicas == nil {
		replicas := int32(1)
		testcustom.Spec.Replicas = &replicas
	}`)

			err = os.WriteFile(webhookFile, []byte(modifiedWebhook), 0644)
			Expect(err).NotTo(HaveOccurred())

			By("adding validation webhook WITHOUT --force")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestCustom",
				"--programmatic-validation",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying validator was added to test file")
			testContent, err = os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(testContent)).To(ContainSubstring("validator TestCustomCustomValidator"))
			Expect(string(testContent)).To(ContainSubstring("Context(\"When creating or updating TestCustom under Validating Webhook\""))

			By("verifying user's renamed variables are preserved")
			Expect(string(testContent)).To(ContainSubstring("myObj     *testv1.TestCustom"))
			Expect(string(testContent)).To(ContainSubstring("oldMyObj  *testv1.TestCustom"))

			By("verifying user's custom BeforeEach code is preserved")
			Expect(string(testContent)).To(ContainSubstring("myObj.Name = \"my-test-object\""))
			Expect(string(testContent)).To(ContainSubstring("myObj.Namespace = \"test-ns\""))
			Expect(string(testContent)).To(ContainSubstring("oldMyObj.Name = \"old-object\""))

			By("verifying user's webhook implementation is preserved")
			webhookContent, err = os.ReadFile(webhookFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(webhookContent)).To(ContainSubstring("// My custom defaulting logic"))
			Expect(string(webhookContent)).To(ContainSubstring("testcustom.Spec.Replicas = &replicas"))

			By("verifying validator implementation was added")
			Expect(string(webhookContent)).To(ContainSubstring("type TestCustomCustomValidator struct"))
			Expect(string(webhookContent)).To(ContainSubstring("func (v *TestCustomCustomValidator) ValidateCreate"))
		})

		It("should work when user removes TODO comments", func() {
			By("creating an API")
			err := kbc.CreateAPI(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestNoTODO",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating validation webhook")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestNoTODO",
				"--programmatic-validation",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("simulating user removing TODO comments")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testnotodo_webhook_test.go")
			content, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			modified := strings.ReplaceAll(string(content), "// TODO (user): Add any setup logic common to all tests\n", "")
			modified = strings.ReplaceAll(modified, "// TODO (user): Add any teardown logic common to all tests\n", "")

			err = os.WriteFile(testFile, []byte(modified), 0644)
			Expect(err).NotTo(HaveOccurred())

			By("adding defaulting webhook WITHOUT --force")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestNoTODO",
				"--defaulting",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying defaulter was added despite missing TODO comments")
			content, err = os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("defaulter TestNoTODOCustomDefaulter"))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestNoTODO under Defaulting Webhook\""))
		})

		It("should support adding defaulting/validation to existing conversion webhook", func() {
			By("creating API v1")
			err := kbc.CreateAPI(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestMultiversion",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating API v2")
			err = kbc.CreateAPI(
				"--group", "test",
				"--version", "v2",
				"--kind", "TestMultiversion",
				"--resource=false", "--controller=false",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating conversion webhook")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestMultiversion",
				"--conversion",
				"--spoke", "v2",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying only conversion test context exists initially")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/testmultiversion_webhook_test.go")
			content, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestMultiversion under Conversion Webhook\""))
			Expect(string(content)).NotTo(ContainSubstring("defaulter"))
			Expect(string(content)).NotTo(ContainSubstring("validator"))

			By("adding defaulting and validation webhooks WITHOUT --force")
			err = kbc.CreateWebhook(
				"--group", "test",
				"--version", "v1",
				"--kind", "TestMultiversion",
				"--defaulting",
				"--programmatic-validation",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying all three webhook types are now present")
			content, err = os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("defaulter TestMultiversionCustomDefaulter"))
			Expect(string(content)).To(ContainSubstring("validator TestMultiversionCustomValidator"))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestMultiversion under Conversion Webhook\""))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating TestMultiversion under Defaulting Webhook\""))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating or updating TestMultiversion under Validating Webhook\""))
		})

		It("should correctly scaffold conversion webhook and storage version marker", func() {
			By("creating API v1")
			err := kbc.CreateAPI(
				"--group", "batch",
				"--version", "v1",
				"--kind", "CronJob",
				"--resource", "--controller",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating defaulting and validation webhooks for v1")
			err = kbc.CreateWebhook(
				"--group", "batch",
				"--version", "v1",
				"--kind", "CronJob",
				"--defaulting",
				"--programmatic-validation",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("creating API v2 without controller")
			err = kbc.CreateAPI(
				"--group", "batch",
				"--version", "v2",
				"--kind", "CronJob",
				"--resource=false", "--controller=false",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("adding conversion webhook to v1 WITHOUT --force")
			err = kbc.CreateWebhook(
				"--group", "batch",
				"--version", "v1",
				"--kind", "CronJob",
				"--conversion",
				"--spoke", "v2",
				"--make=false",
			)
			Expect(err).NotTo(HaveOccurred())

			By("verifying v1 has all three webhook test contexts")
			testFile := filepath.Join(kbc.Dir, "internal/webhook/v1/cronjob_webhook_test.go")
			content, err := os.ReadFile(testFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(content)).To(ContainSubstring("Context(\"When creating CronJob under Defaulting Webhook\""))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating or updating CronJob under Validating Webhook\""))
			Expect(string(content)).To(ContainSubstring("Context(\"When creating CronJob under Conversion Webhook\""))

			By("verifying hub and spoke files were created")
			hubFile := filepath.Join(kbc.Dir, "api/v1/cronjob_conversion.go")
			spokeFile := filepath.Join(kbc.Dir, "api/v2/cronjob_conversion.go")

			_, err = os.Stat(hubFile)
			Expect(err).NotTo(HaveOccurred(), "Hub file should exist")

			_, err = os.Stat(spokeFile)
			Expect(err).NotTo(HaveOccurred(), "Spoke file should exist")

			By("verifying storage version marker was added")
			typesFile := filepath.Join(kbc.Dir, "api/v1/cronjob_types.go")
			typesContent, err := os.ReadFile(typesFile)
			Expect(err).NotTo(HaveOccurred())

			Expect(string(typesContent)).To(ContainSubstring("// +kubebuilder:storageversion"))
		})
	})
})


================================================
FILE: pkg/plugins/golang/v4/suite_test.go
================================================
/*
Copyright 2026 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 v4

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestV4Plugin(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Go V4 Plugin Suite")
}


================================================
FILE: pkg/plugins/golang/v4/webhook.go
================================================
/*
Copyright 2022 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 v4

import (
	"errors"
	"fmt"
	log "log/slog"
	"strings"

	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4/scaffolds"
)

var _ plugin.CreateWebhookSubcommand = &createWebhookSubcommand{}

type createWebhookSubcommand struct {
	config config.Config
	// For help text.
	commandName string

	options *goPlugin.Options

	resource *resource.Resource

	// force indicates that the resource should be created even if it already exists
	force bool

	// Deprecated - TODO: remove it for go/v5
	// isLegacyPath indicates that the resource should be created in the legacy path under the api
	isLegacyPath bool

	// runMake indicates whether to run make or not after scaffolding APIs
	runMake bool
}

func (p *createWebhookSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	p.commandName = cliMeta.CommandName

	subcmdMeta.Description = `Scaffold a webhook for an API resource. You can choose to scaffold defaulting,
validating and/or conversion webhooks.
`
	subcmdMeta.Examples = fmt.Sprintf(`  # Create defaulting and validating webhooks for Group: ship, Version: v1beta1
  # and Kind: Frigate
  %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting --programmatic-validation

  # Create conversion webhook for Group: ship, Version: v1beta1
  # and Kind: Frigate
  %[1]s create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1

  # Create defaulting webhook with custom path for Group: ship, Version: v1beta1
  # and Kind: Frigate
  %[1]s create webhook --group ship --version v1beta1 --kind Frigate --defaulting \
    --defaulting-path=/my-custom-mutate-path
  
  # Create validation webhook with custom path for Group: ship, Version: v1beta1
  # and Kind: Frigate
  %[1]s create webhook --group ship --version v1beta1 --kind Frigate \
    --programmatic-validation --validation-path=/my-custom-validate-path
  
  # Create both defaulting and validation webhooks with different custom paths
  %[1]s create webhook --group ship --version v1beta1 --kind Frigate \
    --defaulting --programmatic-validation \
    --defaulting-path=/custom-mutate --validation-path=/custom-validate
`, cliMeta.CommandName)
}

func (p *createWebhookSubcommand) BindFlags(fs *pflag.FlagSet) {
	p.options = &goPlugin.Options{}

	fs.BoolVar(&p.runMake, "make", true, "if true, run `make generate` after generating files")

	fs.StringVar(&p.options.Plural, "plural", "", "resource irregular plural form")

	fs.BoolVar(&p.options.DoDefaulting, "defaulting", false,
		"if set, scaffold the defaulting webhook")
	fs.BoolVar(&p.options.DoValidation, "programmatic-validation", false,
		"if set, scaffold the validating webhook")
	fs.BoolVar(&p.options.DoConversion, "conversion", false,
		"if set, scaffold the conversion webhook")

	fs.StringSliceVar(&p.options.Spoke, "spoke",
		nil,
		"Comma-separated list of spoke versions to be added to the conversion webhook (e.g., --spoke v1,v2)")

	fs.StringVar(&p.options.DefaultingPath, "defaulting-path", "",
		"Custom path for the defaulting/mutating webhook (only valid with --defaulting)")

	fs.StringVar(&p.options.ValidationPath, "validation-path", "",
		"Custom path for the validation webhook (only valid with --programmatic-validation)")

	// TODO: remove for go/v5
	fs.BoolVar(&p.isLegacyPath, "legacy", false,
		"[DEPRECATED] Attempts to create resource under the API directory (legacy path). "+
			"This option will be removed in future versions.")

	fs.StringVar(&p.options.ExternalAPIPath, "external-api-path", "",
		"Specify the Go package import path for the external API. This is used to scaffold webhooks for resources "+
			"defined outside this project (e.g., github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1).")

	fs.StringVar(&p.options.ExternalAPIDomain, "external-api-domain", "",
		"Specify the domain name for the external API. This domain is used to generate accurate RBAC "+
			"markers and permissions for the external resources (e.g., cert-manager.io).")

	fs.StringVar(&p.options.ExternalAPIModule, "external-api-module", "",
		"external API module with optional version (e.g., github.com/cert-manager/cert-manager@v1.18.2)")

	fs.BoolVar(&p.force, "force", false,
		"attempt to create resource even if it already exists")
}

func (p *createWebhookSubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *createWebhookSubcommand) InjectResource(res *resource.Resource) error {
	p.resource = res

	if len(p.options.ExternalAPIPath) != 0 && len(p.options.ExternalAPIDomain) != 0 && p.isLegacyPath {
		return errors.New("you cannot scaffold webhooks for external types using the legacy path")
	}

	for _, spoke := range p.options.Spoke {
		spoke = strings.TrimSpace(spoke)
		if !isValidVersion(spoke, res, p.config) {
			return fmt.Errorf("invalid spoke version %q", spoke)
		}
		res.Webhooks.Spoke = append(res.Webhooks.Spoke, spoke)
	}

	// Validate path flags are only used with appropriate webhook types
	if p.options.DefaultingPath != "" && !p.options.DoDefaulting {
		return fmt.Errorf("--defaulting-path can only be used with --defaulting")
	}
	if p.options.ValidationPath != "" && !p.options.DoValidation {
		return fmt.Errorf("--validation-path can only be used with --programmatic-validation")
	}

	// Validate that --external-api-module requires --external-api-path
	if len(p.options.ExternalAPIModule) != 0 && len(p.options.ExternalAPIPath) == 0 {
		return errors.New("'--external-api-module' requires '--external-api-path' to be specified")
	}

	p.options.UpdateResource(p.resource, p.config)

	if err := p.resource.Validate(); err != nil {
		return fmt.Errorf("error validating resource: %w", err)
	}

	if !p.resource.HasDefaultingWebhook() && !p.resource.HasValidationWebhook() && !p.resource.HasConversionWebhook() {
		return fmt.Errorf("%s create webhook requires at least one of --defaulting,"+
			" --programmatic-validation and --conversion to be true", p.commandName)
	}

	// check if resource exist to create webhook
	resValue, err := p.config.GetResource(p.resource.GVK)
	res = &resValue
	if err != nil {
		if !p.resource.External && !p.resource.Core {
			return fmt.Errorf("%s create webhook requires a previously created API ", p.commandName)
		}
	} else if res.Webhooks != nil && !res.Webhooks.IsEmpty() && !p.force {
		// Check if user is trying to add a webhook type that already exists
		if p.resource.HasDefaultingWebhook() && res.Webhooks.Defaulting {
			return fmt.Errorf("defaulting webhook already exists for this resource")
		}
		if p.resource.HasValidationWebhook() && res.Webhooks.Validation {
			return fmt.Errorf("validation webhook already exists for this resource")
		}
		if p.resource.HasConversionWebhook() && res.Webhooks.Conversion {
			return fmt.Errorf("conversion webhook already exists for this resource")
		}
		// If we're here, user is adding a new webhook type to existing resource
		// Merge the webhook configurations
		if err := p.resource.Webhooks.Update(res.Webhooks); err != nil {
			return fmt.Errorf("error merging webhook configurations: %w", err)
		}
	}

	return nil
}

func (p *createWebhookSubcommand) Scaffold(fs machinery.Filesystem) error {
	scaffolder := scaffolds.NewWebhookScaffolder(p.config, *p.resource, p.force, p.isLegacyPath)
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("failed to scaffold webhook: %w", err)
	}

	return nil
}

func (p *createWebhookSubcommand) PostScaffold() error {
	// If external API with module specified, add it using go get
	if p.resource.IsExternal() && p.resource.Module != "" {
		log.Info("Adding external API dependency", "module", p.resource.Module)
		// Use go get to add the dependency cleanly as a direct requirement
		err := pluginutil.RunCmd("Add external API dependency", "go", "get", p.resource.Module)
		if err != nil {
			return fmt.Errorf("error adding external API dependency: %w", err)
		}
	}

	err := pluginutil.RunCmd("Update dependencies", "go", "mod", "tidy")
	if err != nil {
		return fmt.Errorf("error updating go dependencies: %w", err)
	}

	if p.runMake {
		err = pluginutil.RunCmd("Running make", "make", "generate")
		if err != nil {
			return fmt.Errorf("error running make generate: %w", err)
		}
	}

	fmt.Print("Next: implement your new Webhook and generate the manifests with:\n$ make manifests\n")

	return nil
}

// Helper function to validate spoke versions
func isValidVersion(version string, res *resource.Resource, cfg config.Config) bool {
	// Fetch all resources in the config
	resources, err := cfg.GetResources()
	if err != nil {
		return false
	}

	// Iterate through resources and validate if the given version exists for the same Group and Kind
	for _, r := range resources {
		if r.Group == res.Group && r.Kind == res.Kind && r.Version == version {
			return true
		}
	}

	// If no matching version is found, return false
	return false
}


================================================
FILE: pkg/plugins/golang/v4/webhook_test.go
================================================
/*
Copyright 2026 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 v4

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/resource"
	goPlugin "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
)

var _ = Describe("createWebhookSubcommand", func() {
	var (
		subCmd *createWebhookSubcommand
		cfg    config.Config
		res    *resource.Resource
	)

	BeforeEach(func() {
		subCmd = &createWebhookSubcommand{}
		cfg = cfgv3.New()
		_ = cfg.SetRepository("github.com/example/test")

		subCmd.options = &goPlugin.Options{}
		res = &resource.Resource{
			GVK: resource.GVK{
				Group:   "crew",
				Domain:  "test.io",
				Version: "v1",
				Kind:    "Captain",
			},
			Plural:   "captains",
			Webhooks: &resource.Webhooks{},
		}
	})

	It("should reject defaulting-path without --defaulting", func() {
		subCmd.options.DefaultingPath = "/custom-path"
		subCmd.options.DoDefaulting = false

		err := subCmd.InjectResource(res)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("--defaulting-path can only be used with --defaulting"))
	})

	It("should reject validation-path without --programmatic-validation", func() {
		subCmd.options.ValidationPath = "/custom-path"
		subCmd.options.DoValidation = false

		err := subCmd.InjectResource(res)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("--validation-path can only be used with --programmatic-validation"))
	})

	It("should require external-api-path when using external-api-module", func() {
		subCmd.options.ExternalAPIModule = "github.com/external/api@v1.0.0"
		subCmd.options.ExternalAPIPath = ""
		subCmd.options.DoDefaulting = true

		err := subCmd.InjectResource(res)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("requires '--external-api-path'"))
	})

	Context("isValidVersion", func() {
		BeforeEach(func() {
			res = &resource.Resource{
				GVK: resource.GVK{
					Group:   "crew",
					Domain:  "test.io",
					Version: "v1",
					Kind:    "Captain",
				},
			}

			for _, version := range []string{"v1", "v2", "v1beta1"} {
				r := resource.Resource{
					GVK: resource.GVK{
						Group:   "crew",
						Domain:  "test.io",
						Version: version,
						Kind:    "Captain",
					},
					API: &resource.API{CRDVersion: "v1"},
				}
				Expect(cfg.AddResource(r)).To(Succeed())
			}
		})

		It("should return true for existing version with same group and kind", func() {
			Expect(isValidVersion("v2", res, cfg)).To(BeTrue())
			Expect(isValidVersion("v1beta1", res, cfg)).To(BeTrue())
		})

		It("should return false for non-existing version", func() {
			Expect(isValidVersion("v3", res, cfg)).To(BeFalse())
		})

		It("should return false for different group", func() {
			differentRes := resource.Resource{
				GVK: resource.GVK{
					Group:   "ship",
					Domain:  "test.io",
					Version: "v1",
					Kind:    "Frigate",
				},
				API: &resource.API{CRDVersion: "v1"},
			}
			Expect(cfg.AddResource(differentRes)).To(Succeed())

			otherRes := &resource.Resource{GVK: differentRes.GVK}
			Expect(isValidVersion("v2", otherRes, cfg)).To(BeFalse())
		})

		It("should return false for different kind", func() {
			differentRes := resource.Resource{
				GVK: resource.GVK{
					Group:   "crew",
					Domain:  "test.io",
					Version: "v1",
					Kind:    "Pirate",
				},
				API: &resource.API{CRDVersion: "v1"},
			}
			Expect(cfg.AddResource(differentRes)).To(Succeed())

			otherRes := &resource.Resource{GVK: differentRes.GVK}
			Expect(isValidVersion("v2", otherRes, cfg)).To(BeFalse())
		})
	})
})


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/edit.go
================================================
/*
Copyright 2025 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 v1alpha

import (
	"fmt"
	log "log/slog"

	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha/scaffolds"
)

var _ plugin.EditSubcommand = &editSubcommand{}

type editSubcommand struct {
	config      config.Config
	useGHModels bool
}

func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = metaDataDescription

	subcmdMeta.Examples = fmt.Sprintf(`  # Edit a common project with this plugin
  %[1]s edit --plugins=%[2]s

  # Edit a common project with GitHub Models enabled (requires repo permissions)
  %[1]s edit --plugins=%[2]s --use-gh-models
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
	fs.BoolVar(&p.useGHModels, "use-gh-models", false,
		"enable GitHub Models AI summary in the scaffolded workflow (requires GitHub Models permissions)")
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *editSubcommand) PreScaffold(machinery.Filesystem) error {
	if len(p.config.GetCliVersion()) == 0 {
		return fmt.Errorf(
			"you must manually upgrade your project to a version that records the CLI version in PROJECT (`cliVersion`) " +
				"to allow the `alpha update` command to work properly before using this plugin.\n" +
				"More info: https://book.kubebuilder.io/migrations",
		)
	}
	return nil
}

func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
	if err := insertPluginMetaToConfig(p.config, PluginConfig{UseGHModels: p.useGHModels}); err != nil {
		return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
	}

	scaffolder := scaffolds.NewInitScaffolder(p.useGHModels)
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("error scaffolding edit subcommand: %w", err)
	}

	return nil
}

func (p *editSubcommand) PostScaffold() error {
	// Inform users about GitHub Models if they didn't enable it
	if !p.useGHModels {
		log.Info("Consider enabling GitHub Models to get an AI summary to help with the update")
		log.Info("Use the --use-gh-models flag if your project/organization has permission to use GitHub Models")
	}
	return nil
}


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/edit_test.go
================================================
/*
Copyright 2026 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 v1alpha

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("editSubcommand", func() {
	var (
		subCmd *editSubcommand
		cfg    config.Config
		fs     machinery.Filesystem
	)

	BeforeEach(func() {
		subCmd = &editSubcommand{}
		cfg = cfgv3.New()
		fs = machinery.Filesystem{FS: afero.NewMemMapFs()}
		Expect(subCmd.InjectConfig(cfg)).To(Succeed())
	})

	It("should require cliVersion to be set in PROJECT file", func() {
		err := subCmd.PreScaffold(fs)

		Expect(err).To(HaveOccurred())
		Expect(err.Error()).To(ContainSubstring("must manually upgrade your project"))
		Expect(err.Error()).To(ContainSubstring("cliVersion"))
	})

	It("should succeed when cliVersion is set", func() {
		Expect(cfg.SetCliVersion("v4.0.0")).To(Succeed())

		err := subCmd.PreScaffold(fs)

		Expect(err).NotTo(HaveOccurred())
	})
})


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/init.go
================================================
/*
Copyright 2025 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 v1alpha

import (
	"fmt"
	log "log/slog"

	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha/scaffolds"
)

var _ plugin.InitSubcommand = &initSubcommand{}

type initSubcommand struct {
	config      config.Config
	useGHModels bool
}

func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = metaDataDescription

	subcmdMeta.Examples = fmt.Sprintf(`  # Initialize a common project with this plugin
  %[1]s init --plugins=%[2]s

  # Initialize with GitHub Models enabled (requires repo permissions)
  %[1]s init --plugins=%[2]s --use-gh-models
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *initSubcommand) BindFlags(fs *pflag.FlagSet) {
	fs.BoolVar(&p.useGHModels, "use-gh-models", false,
		"enable GitHub Models AI summary in the scaffolded workflow (requires GitHub Models permissions)")
}

func (p *initSubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
	if err := insertPluginMetaToConfig(p.config, PluginConfig{UseGHModels: p.useGHModels}); err != nil {
		return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
	}

	scaffolder := scaffolds.NewInitScaffolder(p.useGHModels)
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("error scaffolding init subcommand: %w", err)
	}

	return nil
}

func (p *initSubcommand) PostScaffold() error {
	// Inform users about GitHub Models if they didn't enable it
	if !p.useGHModels {
		log.Info("Consider enabling GitHub Models to get an AI summary to help with the update")
		log.Info("Use the --use-gh-models flag if your project/organization has permission to use GitHub Models")
	}
	return nil
}


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/plugin.go
================================================
/*
Copyright 2025 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 v1alpha

import (
	"errors"
	"fmt"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
)

//nolint:lll
const metaDataDescription = `This plugin scaffolds a GitHub Action that helps you keep your project aligned with the latest Kubebuilder improvements. With a tiny amount of setup, you'll receive **automatic issue notifications** whenever a new Kubebuilder release is available. Each issue includes a **compare link** so you can open a Pull Request with one click and review the changes safely.

Under the hood, the workflow runs 'kubebuilder alpha update' using a **3-way merge strategy** to refresh your scaffold while preserving your code. It creates and pushes an update branch, then opens a GitHub **Issue** containing the PR URL you can use to review and merge.

### How to set it up

1) **Add the plugin**: Use the Kubebuilder CLI to scaffold the automation into your repo.
2) **Review the workflow**: The file '.github/workflows/auto_update.yml' runs on a schedule to check for updates.
3) **Permissions required** (via the built-in 'GITHUB_TOKEN'):
   - **contents: write** — needed to create and push the update branch.
   - **issues: write** — needed to create the tracking Issue with the PR link.
   - **models: read** (optional) — only required if using --use-gh-models flag for AI-generated summaries.
4) **Protect your branches**: Enable **branch protection rules** so automated changes **cannot** be pushed directly. All updates must go through a Pull Request for review.

### Optional: GitHub Models AI Summary

By default, the workflow does NOT use GitHub Models. To enable AI-generated summaries in GitHub issues:
  - Ensure your repository/organization has permissions to use GitHub Models.
  - Re-run: kubebuilder edit --plugins="autoupdate/v1-alpha" --use-gh-models

Without this flag, the workflow will still work but won't include AI summaries (avoiding 403 Forbidden errors).`

const pluginName = "autoupdate." + plugins.DefaultNameQualifier

var (
	pluginVersion            = plugin.Version{Number: 1, Stage: stage.Alpha}
	supportedProjectVersions = []config.Version{cfgv3.Version}
)

// Plugin implements the plugin.Full interface
type Plugin struct {
	editSubcommand
	initSubcommand
}

var _ plugin.Init = Plugin{}

// PluginConfig defines the structure that will be used to track the data
type PluginConfig struct {
	UseGHModels bool `json:"useGHModels,omitempty"`
}

// Name returns the name of the plugin
func (Plugin) Name() string { return pluginName }

// Version returns the version of the Helm plugin
func (Plugin) Version() plugin.Version { return pluginVersion }

// SupportedProjectVersions returns an array with all project versions supported by the plugin
func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }

// GetEditSubcommand will return the subcommand which is responsible for adding and/or edit a autoupdate
func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }

// GetInitSubcommand will return the subcommand which is responsible for init autoupdate plugin
func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }

// Description returns a short description of the plugin
func (Plugin) Description() string {
	return "Proposes Kubebuilder scaffold updates via GitHub Actions"
}

// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
func (p Plugin) DeprecationWarning() string {
	return ""
}

// insertPluginMetaToConfig will insert the metadata to the plugin configuration
func insertPluginMetaToConfig(target config.Config, cfg PluginConfig) error {
	key := plugin.GetPluginKeyForConfig(target.GetPluginChain(), Plugin{})
	canonicalKey := plugin.KeyFor(Plugin{})

	if err := target.DecodePluginConfig(key, &cfg); err != nil {
		switch {
		case errors.As(err, &config.UnsupportedFieldError{}):
			return nil
		case errors.As(err, &config.PluginKeyNotFoundError{}):
			if key != canonicalKey {
				if err2 := target.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
					if errors.As(err2, &config.UnsupportedFieldError{}) {
						return nil
					}
					if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
						return fmt.Errorf("error decoding plugin configuration: %w", err2)
					}
				}
			}
		default:
			return fmt.Errorf("error decoding plugin configuration: %w", err)
		}
	}

	if err := target.EncodePluginConfig(key, cfg); err != nil {
		return fmt.Errorf("error encoding plugin configuration: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/plugin_test.go
================================================
/*
Copyright 2026 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 v1alpha

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
)

var _ = Describe("Plugin", func() {
	var p Plugin

	It("should have correct version and support v3 projects", func() {
		Expect(p.Version().Number).To(Equal(1))
		Expect(p.SupportedProjectVersions()).To(ContainElement(cfgv3.Version))
	})

	It("should not be deprecated", func() {
		Expect(p.DeprecationWarning()).To(BeEmpty())
	})
})


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/scaffolds/init.go
================================================
/*
Copyright 2022 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 scaffolds

import (
	"fmt"
	log "log/slog"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/autoupdate/v1alpha/scaffolds/internal/github"
)

var _ plugins.Scaffolder = &initScaffolder{}

type initScaffolder struct {
	config config.Config

	// fs is the filesystem that will be used by the scaffolder
	fs machinery.Filesystem

	// useGHModels determines if GitHub Models AI summary should be enabled
	useGHModels bool
}

// NewInitScaffolder returns a new Scaffolder for project initialization operations
func NewInitScaffolder(useGHModels bool) plugins.Scaffolder {
	return &initScaffolder{
		useGHModels: useGHModels,
	}
}

// InjectFS implements cmdutil.Scaffolder
func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// Scaffold implements cmdutil.Scaffolder
func (s *initScaffolder) Scaffold() error {
	log.Info("Writing scaffold for you to edit...")

	scaffold := machinery.NewScaffold(s.fs,
		machinery.WithConfig(s.config),
	)

	err := scaffold.Execute(
		&github.AutoUpdate{UseGHModels: s.useGHModels},
	)
	if err != nil {
		return fmt.Errorf("failed to execute init scaffold: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/scaffolds/internal/github/auto_update.go
================================================
/*
Copyright 2025 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 github

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &AutoUpdate{}

// AutoUpdate scaffolds the GitHub Action to lint the project
type AutoUpdate struct {
	machinery.TemplateMixin
	machinery.BoilerplateMixin

	// UseGHModels indicates whether to enable GitHub Models AI summary
	UseGHModels bool
}

// SetTemplateDefaults implements machinery.Template
func (f *AutoUpdate) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join(".github", "workflows", "auto_update.yml")
	}

	f.TemplateBody = autoUpdateTemplate
	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const autoUpdateTemplate = `name: Auto Update

# The 'kubebuilder alpha update' command requires write access to the repository to create a branch
# with the update files and allow you to open a pull request using the link provided in the issue.
# The branch created will be named in the format kubebuilder-update-from--to- by default.
# To protect your codebase, please ensure that you have branch protection rules configured for your 
# main branches. This will guarantee that no one can bypass a review and push directly to a branch like 'main'.
permissions:
  contents: write  # Create and push the update branch
  issues: write    # Create GitHub Issue with PR link{{ if .UseGHModels }}
  models: read     # Use GitHub Models for AI summaries{{ end }}

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * 2" # Every Tuesday at 00:00 UTC

jobs:
  auto-update:
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: {{ "${{ secrets.GITHUB_TOKEN }}" }}

    # Checkout the repository.
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        token: {{ "${{ secrets.GITHUB_TOKEN }}" }}
        fetch-depth: 0

    # Configure Git to create commits with the GitHub Actions bot.
    - name: Configure Git
      run: |
        git config --global user.name "github-actions[bot]"
        git config --global user.email "github-actions[bot]@users.noreply.github.com"

    # Set up Go environment.
    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: stable

    # Install Kubebuilder.
    - name: Install Kubebuilder
      run: |
        curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
        chmod +x kubebuilder
        sudo mv kubebuilder /usr/local/bin/
        kubebuilder version
{{ if .UseGHModels }}
    # Install Models extension for GitHub CLI.
    - name: Install gh-models extension
      run: |
        gh extension install github/gh-models --force
        gh models --help >/dev/null
{{ end }}
    # Run the Kubebuilder alpha update command.
    # More info: https://kubebuilder.io/reference/commands/alpha_update
    - name: Run kubebuilder alpha update
      # Executes the update command with specified flags.
      # --force: Completes the merge even if conflicts occur, leaving conflict markers.
      # --push: Automatically pushes the resulting output branch to the 'origin' remote.
      # --restore-path: Preserves specified paths (e.g., CI workflow files) when squashing.
      # --open-gh-issue: Creates a GitHub Issue with a link for opening a PR for review.{{ if .UseGHModels }}
      # --use-gh-models: Adds an AI-generated comment to the created Issue with
      #   a short overview of the scaffold changes and conflict-resolution guidance (if any).{{ else }}
      #
      # WARNING: This workflow does not use GitHub Models AI summary by default.
      # To enable AI-generated summaries in GitHub issues, you need permissions to use GitHub Models.
      # If you have the required permissions, re-run:
      #   kubebuilder edit --plugins="autoupdate/v1-alpha" --use-gh-models{{ end }}
      run: |
        kubebuilder alpha update \
          --force \
          --push \
          --restore-path .github/workflows \
          --open-gh-issue{{ if .UseGHModels }} \
          --use-gh-models{{ end }}
`


================================================
FILE: pkg/plugins/optional/autoupdate/v1alpha/suite_test.go
================================================
/*
Copyright 2026 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 v1alpha

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestAutoupdateV1Alpha(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Autoupdate V1Alpha Plugin Suite")
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/commons.go
================================================
/*
Copyright 2022 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 v1alpha

import (
	"errors"
	"fmt"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
)

// InsertPluginMetaToConfig will insert the metadata to the plugin configuration
func InsertPluginMetaToConfig(target config.Config, cfg pluginConfig) error {
	key := plugin.GetPluginKeyForConfig(target.GetPluginChain(), Plugin{})
	canonicalKey := plugin.KeyFor(Plugin{})

	if err := target.DecodePluginConfig(key, &cfg); err != nil {
		switch {
		case errors.As(err, &config.UnsupportedFieldError{}):
			return nil
		case errors.As(err, &config.PluginKeyNotFoundError{}):
			if key != canonicalKey {
				if err2 := target.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
					if errors.As(err2, &config.UnsupportedFieldError{}) {
						return nil
					}
					if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
						return fmt.Errorf("error decoding plugin configuration: %w", err2)
					}
				}
			}
		default:
			return fmt.Errorf("error decoding plugin configuration: %w", err)
		}
	}

	if err := target.EncodePluginConfig(key, cfg); err != nil {
		return fmt.Errorf("error encoding plugin configuration: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/constants.go
================================================
/*
Copyright 2022 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 v1alpha

//nolint:lll
const metaDataDescription = `This command will add Grafana manifests to the project:
  - A JSON file includes dashboard manifest that can be directly copied to Grafana Web UI.
	('grafana/controller-runtime-metrics.json')

NOTE: This plugin requires:
- Access to Prometheus
- Your project must be using controller-runtime to expose the metrics via the controller metrics and they need to be collected by Prometheus.
- Access to Grafana (https://grafana.com/docs/grafana/latest/setup-grafana/installation/)
Check how to enable the metrics for your project by looking at the doc: https://book.kubebuilder.io/reference/metrics.html
`


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/edit.go
================================================
/*
Copyright 2022 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.
*/

//nolint:dupl
package v1alpha

import (
	"fmt"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds"
)

var _ plugin.EditSubcommand = &editSubcommand{}

type editSubcommand struct {
	config config.Config
}

func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = metaDataDescription

	subcmdMeta.Examples = fmt.Sprintf(`  # Edit a common project with this plugin
  %[1]s edit --plugins=%[2]s
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
	if err := InsertPluginMetaToConfig(p.config, pluginConfig{}); err != nil {
		return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
	}

	scaffolder := scaffolds.NewEditScaffolder()
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("error scaffolding edit subcommand: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/init.go
================================================
/*
Copyright 2022 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.
*/

//nolint:dupl
package v1alpha

import (
	"fmt"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds"
)

var _ plugin.InitSubcommand = &initSubcommand{}

type initSubcommand struct {
	config config.Config
}

func (p *initSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = metaDataDescription

	subcmdMeta.Examples = fmt.Sprintf(`  # Initialize a common project with this plugin
  %[1]s init --plugins=%[2]s
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *initSubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *initSubcommand) Scaffold(fs machinery.Filesystem) error {
	if err := InsertPluginMetaToConfig(p.config, pluginConfig{}); err != nil {
		return fmt.Errorf("error inserting project plugin meta to configuration: %w", err)
	}

	scaffolder := scaffolds.NewInitScaffolder()
	scaffolder.InjectFS(fs)
	if err := scaffolder.Scaffold(); err != nil {
		return fmt.Errorf("error scaffolding init subcommand: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/plugin.go
================================================
/*
Copyright 2022 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 v1alpha

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
)

const pluginName = "grafana." + plugins.DefaultNameQualifier

var (
	pluginVersion            = plugin.Version{Number: 1, Stage: stage.Alpha}
	supportedProjectVersions = []config.Version{cfgv3.Version}
)

// Plugin implements the plugin.Full interface
type Plugin struct {
	initSubcommand
	editSubcommand
}

var _ plugin.Init = Plugin{}

// Name returns the name of the plugin
func (Plugin) Name() string { return pluginName }

// Version returns the version of the grafana plugin
func (Plugin) Version() plugin.Version { return pluginVersion }

// SupportedProjectVersions returns an array with all project versions supported by the plugin
func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }

// GetInitSubcommand will return the subcommand which is responsible for initializing and scaffolding grafana manifests
func (p Plugin) GetInitSubcommand() plugin.InitSubcommand { return &p.initSubcommand }

// GetEditSubcommand will return the subcommand which is responsible for adding grafana manifests
func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }

type pluginConfig struct{}

// Description returns a short description of the plugin
func (Plugin) Description() string {
	return "Generates Grafana Dashboards for metrics"
}

// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
func (p Plugin) DeprecationWarning() string {
	return ""
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/edit.go
================================================
/*
Copyright 2022 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 scaffolds

import (
	"fmt"
	"io"
	log "log/slog"
	"os"
	"strings"

	"sigs.k8s.io/yaml"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates"
)

var _ plugins.Scaffolder = &editScaffolder{}

const configFilePath = "grafana/custom-metrics/config.yaml"

type editScaffolder struct {
	// fs is the filesystem that will be used by the scaffolder
	fs machinery.Filesystem
}

// NewEditScaffolder returns a new Scaffolder for project edition operations
func NewEditScaffolder() plugins.Scaffolder {
	return &editScaffolder{}
}

// InjectFS implements cmdutil.Scaffolder
func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

func fileExist(configFilePath string) bool {
	if _, err := os.Stat(configFilePath); os.IsNotExist(err) {
		return false
	}
	return true
}

func loadConfig(configPath string) ([]templates.CustomMetricItem, error) {
	if !fileExist(configPath) {
		return nil, nil
	}

	//nolint:gosec
	f, err := os.Open(configPath)
	if err != nil {
		return nil, fmt.Errorf("error loading plugin config: %w", err)
	}

	items, err := configReader(f)
	if err != nil {
		return nil, fmt.Errorf("error reading config.yaml: %w", err)
	}

	if err = f.Close(); err != nil {
		return nil, fmt.Errorf("could not close config.yaml: %w", err)
	}

	return items, nil
}

func configReader(reader io.Reader) ([]templates.CustomMetricItem, error) {
	yamlFile, err := io.ReadAll(reader)
	if err != nil {
		return nil, fmt.Errorf("error reading config.yaml: %w", err)
	}

	config := templates.CustomMetricsConfig{}

	err = yaml.Unmarshal(yamlFile, &config)
	if err != nil {
		return nil, fmt.Errorf("error parsing config.yaml: %w", err)
	}

	validatedMetricItems := validateCustomMetricItems(config.CustomMetrics)

	return validatedMetricItems, nil
}

func validateCustomMetricItems(rawItems []templates.CustomMetricItem) []templates.CustomMetricItem {
	// 1. Filter items of missing `Metric` or `Type`
	var filterResult []templates.CustomMetricItem
	for _, item := range rawItems {
		if hasFields(item) {
			filterResult = append(filterResult, item)
		}
	}

	// 2. Fill Expr and Unit if missing
	validatedItems := make([]templates.CustomMetricItem, len(filterResult))
	for i, item := range filterResult {
		item = fillMissingExpr(item)
		validatedItems[i] = fillMissingUnit(item)
	}

	return validatedItems
}

func hasFields(item templates.CustomMetricItem) bool {
	// If `Expr` exists, return true
	if item.Expr != "" {
		return true
	}

	// If `Metric` & valid `Type` exists, return true
	metricType := strings.ToLower(item.Type)
	if item.Metric != "" && (metricType == "counter" || metricType == "gauge" || metricType == "histogram") {
		return true
	}

	return false
}

// TODO: Prom_ql exprs can improved to be more pratical and applicable
func fillMissingExpr(item templates.CustomMetricItem) templates.CustomMetricItem {
	if item.Expr == "" {
		switch strings.ToLower(item.Type) {
		case "counter":
			item.Expr = "sum(rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)`
		case "histogram":
			//nolint:lll
			item.Expr = "histogram_quantile(0.90, sum by(instance, le) (rate(" + item.Metric + `{job=\"$job\", namespace=\"$namespace\"}[5m])))`
		default: // gauge
			item.Expr = item.Metric
		}
	}
	return item
}

func fillMissingUnit(item templates.CustomMetricItem) templates.CustomMetricItem {
	if item.Unit == "" {
		name := strings.ToLower(item.Metric)
		item.Unit = "none"
		if strings.Contains(name, "second") || strings.Contains(name, "duration") {
			item.Unit = "s"
		} else if strings.Contains(name, "byte") {
			item.Unit = "bytes"
		} else if strings.Contains(name, "ratio") {
			item.Unit = "percent"
		}
	}
	return item
}

// Scaffold implements cmdutil.Scaffolder
func (s *editScaffolder) Scaffold() error {
	log.Info("Generating Grafana manifests to visualize controller status...")

	// Initialize the machinery.Scaffold that will write the files to disk
	scaffold := machinery.NewScaffold(s.fs)

	configPath := configFilePath

	templatesBuilder := []machinery.Builder{
		&templates.RuntimeManifest{},
		&templates.ResourcesManifest{},
		&templates.CustomMetricsConfigManifest{ConfigPath: configPath},
	}

	configItems, err := loadConfig(configPath)
	if err == nil && len(configItems) > 0 {
		templatesBuilder = append(templatesBuilder, &templates.CustomMetricsDashManifest{Items: configItems})
	} else if err != nil {
		_, _ = fmt.Fprintf(os.Stderr, "Error on scaffolding manifest for custom metris:\n%v", err)
	}

	if err = scaffold.Execute(templatesBuilder...); err != nil {
		return fmt.Errorf("error scaffolding Grafana manifests: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/edit_test.go
================================================
//go:build integration

/*
Copyright 2025 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 scaffolds

import (
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates"
)

var _ = Describe("Edit Scaffolder", func() {
	var (
		fs         machinery.Filesystem
		scaffolder *editScaffolder
		tmpDir     string
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "grafana-test-*")
		Expect(err).NotTo(HaveOccurred())

		// Change to tmpDir so relative paths work correctly
		err = os.Chdir(tmpDir)
		Expect(err).NotTo(HaveOccurred())

		fs = machinery.Filesystem{
			FS: afero.NewBasePathFs(afero.NewOsFs(), tmpDir),
		}
		scaffolder = &editScaffolder{}
		scaffolder.InjectFS(fs)
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	Describe("configReader", func() {
		It("should parse valid custom metrics config", func() {
			configContent := `---
customMetrics:
  - metric: foo_bar
    type: counter
  - metric: baz_qux
    type: histogram
`
			reader := strings.NewReader(configContent)
			items, err := configReader(reader)
			Expect(err).NotTo(HaveOccurred())
			Expect(items).To(HaveLen(2))
			Expect(items[0].Metric).To(Equal("foo_bar"))
			Expect(items[0].Type).To(Equal("counter"))
			Expect(items[1].Metric).To(Equal("baz_qux"))
			Expect(items[1].Type).To(Equal("histogram"))
		})

		It("should handle empty config", func() {
			configContent := `---
customMetrics:
`
			reader := strings.NewReader(configContent)
			items, err := configReader(reader)
			Expect(err).NotTo(HaveOccurred())
			Expect(items).To(BeEmpty())
		})

		It("should return error for invalid YAML", func() {
			configContent := `invalid: yaml: content:`
			reader := strings.NewReader(configContent)
			_, err := configReader(reader)
			Expect(err).To(HaveOccurred())
		})
	})

	Describe("validateCustomMetricItems", func() {
		It("should filter out items missing required fields", func() {
			items := []templates.CustomMetricItem{
				{Metric: "valid_metric", Type: "counter"},
				{Metric: "", Type: "gauge"}, // Missing metric
				{Metric: "another_valid", Type: "histogram"},
				{Type: "counter"}, // Missing metric
			}

			validated := validateCustomMetricItems(items)
			Expect(validated).To(HaveLen(2))
			Expect(validated[0].Metric).To(Equal("valid_metric"))
			Expect(validated[1].Metric).To(Equal("another_valid"))
		})

		It("should fill missing expr for counter type", func() {
			items := []templates.CustomMetricItem{
				{Metric: "foo_bar", Type: "counter"},
			}

			validated := validateCustomMetricItems(items)
			Expect(validated).To(HaveLen(1))
			Expect(validated[0].Expr).To(ContainSubstring("sum(rate(foo_bar"))
			Expect(validated[0].Expr).To(ContainSubstring(`{job=\"$job\", namespace=\"$namespace\"}`))
		})

		It("should fill missing expr for histogram type", func() {
			items := []templates.CustomMetricItem{
				{Metric: "foo_bar", Type: "histogram"},
			}

			validated := validateCustomMetricItems(items)
			Expect(validated).To(HaveLen(1))
			Expect(validated[0].Expr).To(ContainSubstring("histogram_quantile(0.90"))
			Expect(validated[0].Expr).To(ContainSubstring("foo_bar"))
		})

		It("should fill missing expr for gauge type", func() {
			items := []templates.CustomMetricItem{
				{Metric: "foo_bar", Type: "gauge"},
			}

			validated := validateCustomMetricItems(items)
			Expect(validated).To(HaveLen(1))
			Expect(validated[0].Expr).To(Equal("foo_bar"))
		})

		It("should not override existing expr", func() {
			customExpr := "my_custom_expr"
			items := []templates.CustomMetricItem{
				{Metric: "foo_bar", Type: "counter", Expr: customExpr},
			}

			validated := validateCustomMetricItems(items)
			Expect(validated).To(HaveLen(1))
			Expect(validated[0].Expr).To(Equal(customExpr))
		})
	})

	Describe("hasFields", func() {
		It("should return true when expr exists", func() {
			item := templates.CustomMetricItem{Expr: "some_expr"}
			Expect(hasFields(item)).To(BeTrue())
		})

		It("should return true when metric and valid type exist", func() {
			validTypes := []string{"counter", "gauge", "histogram"}
			for _, t := range validTypes {
				item := templates.CustomMetricItem{Metric: "foo_bar", Type: t}
				Expect(hasFields(item)).To(BeTrue(), "Expected type %s to be valid", t)
			}
		})

		It("should return false when metric is missing", func() {
			item := templates.CustomMetricItem{Type: "counter"}
			Expect(hasFields(item)).To(BeFalse())
		})

		It("should return false when type is invalid", func() {
			item := templates.CustomMetricItem{Metric: "foo_bar", Type: "invalid"}
			Expect(hasFields(item)).To(BeFalse())
		})

		It("should return false when both metric and expr are missing", func() {
			item := templates.CustomMetricItem{Type: "counter"}
			Expect(hasFields(item)).To(BeFalse())
		})
	})

	Describe("fillMissingUnit", func() {
		It("should detect seconds unit", func() {
			items := []templates.CustomMetricItem{
				{Metric: "foo_seconds"},
				{Metric: "bar_duration"},
			}

			for _, item := range items {
				filled := fillMissingUnit(item)
				Expect(filled.Unit).To(Equal("s"))
			}
		})

		It("should detect bytes unit", func() {
			item := templates.CustomMetricItem{Metric: "foo_bytes"}
			filled := fillMissingUnit(item)
			Expect(filled.Unit).To(Equal("bytes"))
		})

		It("should detect percent unit", func() {
			item := templates.CustomMetricItem{Metric: "foo_ratio"}
			filled := fillMissingUnit(item)
			Expect(filled.Unit).To(Equal("percent"))
		})

		It("should default to none for unknown units", func() {
			item := templates.CustomMetricItem{Metric: "foo_bar"}
			filled := fillMissingUnit(item)
			Expect(filled.Unit).To(Equal("none"))
		})

		It("should not override existing unit", func() {
			item := templates.CustomMetricItem{Metric: "foo_bar", Unit: "custom"}
			filled := fillMissingUnit(item)
			Expect(filled.Unit).To(Equal("custom"))
		})
	})

	Describe("Scaffold", func() {
		Context("when initializing a project with grafana plugin", func() {
			It("should scaffold the default grafana manifests", func() {
				err := scaffolder.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("creating the controller-runtime metrics dashboard")
				runtimePath := filepath.Join("grafana", "controller-runtime-metrics.json")
				Expect(fileExists(runtimePath)).To(BeTrue())
				content, err := os.ReadFile(runtimePath)
				Expect(err).NotTo(HaveOccurred())
				Expect(string(content)).To(ContainSubstring("controller_runtime"))

				By("creating the controller resources metrics dashboard")
				resourcesPath := filepath.Join("grafana", "controller-resources-metrics.json")
				Expect(fileExists(resourcesPath)).To(BeTrue())

				By("creating the custom metrics config template")
				configPath := configFilePath
				Expect(fileExists(configPath)).To(BeTrue())
				content, err = os.ReadFile(configPath)
				Expect(err).NotTo(HaveOccurred())
				Expect(string(content)).To(ContainSubstring("customMetrics:"))
				Expect(string(content)).To(ContainSubstring("# Example:"))
			})
		})

		Context("when editing a project with custom metrics", func() {
			BeforeEach(func() {
				// First scaffold to create initial structure
				err := scaffolder.Scaffold()
				Expect(err).NotTo(HaveOccurred())
			})

			It("should generate custom metrics dashboard for counter and histogram of same metric", func() {
				By("updating the config with counter and histogram for same metric name")
				configContent := `---
customMetrics:
  - metric: foo_bar
    type: counter
  - metric: foo_bar
    type: histogram
`
				configPath := configFilePath
				err := os.WriteFile(configPath, []byte(configContent), 0o644)
				Expect(err).NotTo(HaveOccurred())

				By("running scaffold again to generate the custom metrics dashboard")
				scaffolder2 := &editScaffolder{}
				scaffolder2.InjectFS(fs)
				err = scaffolder2.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying the custom metrics dashboard was created")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				Expect(fileExists(dashPath)).To(BeTrue())

				By("verifying the dashboard contains the counter expression")
				content, err := os.ReadFile(dashPath)
				Expect(err).NotTo(HaveOccurred())
				contentStr := string(content)
				expectedCounter := `sum(rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)`
				Expect(contentStr).To(ContainSubstring(expectedCounter),
					"Dashboard should contain counter expression")

				By("verifying the dashboard contains the histogram expression")
				expectedHistogram := `histogram_quantile(0.90, sum by(instance, le) ` +
					`(rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m])))`
				Expect(contentStr).To(ContainSubstring(expectedHistogram),
					"Dashboard should contain histogram expression")
			})

			It("should generate dashboard with multiple different metrics", func() {
				By("configuring multiple different metrics")
				configContent := `---
customMetrics:
  - metric: http_requests_total
    type: counter
  - metric: memory_usage_bytes
    type: gauge
  - metric: request_duration_seconds
    type: histogram
`
				configPath := configFilePath
				err := os.WriteFile(configPath, []byte(configContent), 0o644)
				Expect(err).NotTo(HaveOccurred())

				By("generating the dashboard")
				scaffolder2 := &editScaffolder{}
				scaffolder2.InjectFS(fs)
				err = scaffolder2.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying all metrics are present in the dashboard")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				content, err := os.ReadFile(dashPath)
				Expect(err).NotTo(HaveOccurred())
				contentStr := string(content)

				Expect(contentStr).To(ContainSubstring("http_requests_total"))
				Expect(contentStr).To(ContainSubstring("memory_usage_bytes"))
				Expect(contentStr).To(ContainSubstring("request_duration_seconds"))
				Expect(contentStr).To(ContainSubstring("histogram_quantile"))
			})

			It("should handle metrics with custom expressions", func() {
				By("configuring metrics with custom expressions")
				configContent := `---
customMetrics:
  - metric: custom_metric
    type: counter
    expr: 'my_custom_expression{label="value"}'
    unit: custom_unit
`
				configPath := configFilePath
				err := os.WriteFile(configPath, []byte(configContent), 0o644)
				Expect(err).NotTo(HaveOccurred())

				By("generating the dashboard")
				scaffolder2 := &editScaffolder{}
				scaffolder2.InjectFS(fs)
				err = scaffolder2.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying the custom expression is used")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				content, err := os.ReadFile(dashPath)
				Expect(err).NotTo(HaveOccurred())
				contentStr := string(content)

				Expect(contentStr).To(ContainSubstring(`my_custom_expression{label="value"}`))
				Expect(contentStr).To(ContainSubstring("custom_unit"))
			})

			It("should auto-detect units from metric names", func() {
				By("configuring metrics with unit-indicating names")
				configContent := `---
customMetrics:
  - metric: response_time_seconds
    type: gauge
  - metric: memory_bytes
    type: gauge
  - metric: error_ratio
    type: gauge
`
				configPath := configFilePath
				err := os.WriteFile(configPath, []byte(configContent), 0o644)
				Expect(err).NotTo(HaveOccurred())

				By("generating the dashboard")
				scaffolder2 := &editScaffolder{}
				scaffolder2.InjectFS(fs)
				err = scaffolder2.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying units were auto-detected")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				content, err := os.ReadFile(dashPath)
				Expect(err).NotTo(HaveOccurred())
				contentStr := string(content)

				// The dashboard should contain the auto-detected units
				Expect(contentStr).To(ContainSubstring(`"unit": "s"`))       // seconds
				Expect(contentStr).To(ContainSubstring(`"unit": "bytes"`))   // bytes
				Expect(contentStr).To(ContainSubstring(`"unit": "percent"`)) // percent
			})

			It("should skip invalid metrics and only process valid ones", func() {
				By("configuring a mix of valid and invalid metrics")
				configContent := `---
customMetrics:
  - metric: valid_counter
    type: counter
  - metric: ""
    type: gauge
  - type: histogram
  - metric: another_valid
    type: gauge
  - metric: invalid_type
    type: unknown
`
				configPath := configFilePath
				err := os.WriteFile(configPath, []byte(configContent), 0o644)
				Expect(err).NotTo(HaveOccurred())

				By("generating the dashboard")
				scaffolder2 := &editScaffolder{}
				scaffolder2.InjectFS(fs)
				err = scaffolder2.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying dashboard was created with only valid metrics")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				if fileExists(dashPath) {
					content, err := os.ReadFile(dashPath)
					Expect(err).NotTo(HaveOccurred())
					contentStr := string(content)

					Expect(contentStr).To(ContainSubstring("valid_counter"))
					Expect(contentStr).To(ContainSubstring("another_valid"))
					Expect(contentStr).NotTo(ContainSubstring("invalid_type"))
				}
			})

			It("should handle metrics ending with _info suffix specially", func() {
				By("configuring a metric with _info suffix")
				configContent := `---
customMetrics:
  - metric: build_info
    type: gauge
`
				configPath := configFilePath
				err := os.WriteFile(configPath, []byte(configContent), 0o644)
				Expect(err).NotTo(HaveOccurred())

				By("generating the dashboard")
				scaffolder2 := &editScaffolder{}
				scaffolder2.InjectFS(fs)
				err = scaffolder2.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying dashboard was created with _info metric using table visualization")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				if fileExists(dashPath) {
					content, err := os.ReadFile(dashPath)
					Expect(err).NotTo(HaveOccurred())
					contentStr := string(content)

					Expect(contentStr).To(ContainSubstring("build_info"))
					Expect(contentStr).To(ContainSubstring(`"type": "table"`))
				}
			})
		})

		Context("when no custom metrics are configured", func() {
			It("should not create custom metrics dashboard", func() {
				By("scaffolding with default config")
				err := scaffolder.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying custom metrics dashboard was not created")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				Expect(fileExists(dashPath)).To(BeFalse())

				By("verifying the config template was created")
				configPath := configFilePath
				Expect(fileExists(configPath)).To(BeTrue())
			})
		})

		Context("when config file has only empty metrics", func() {
			It("should not create custom metrics dashboard", func() {
				By("creating empty config")
				configContent := `---
customMetrics: []
`
				err := os.MkdirAll(filepath.Join("grafana", "custom-metrics"), 0o755)
				Expect(err).NotTo(HaveOccurred())

				configPath := configFilePath
				err = os.WriteFile(configPath, []byte(configContent), 0o644)
				Expect(err).NotTo(HaveOccurred())

				By("scaffolding")
				err = scaffolder.Scaffold()
				Expect(err).NotTo(HaveOccurred())

				By("verifying custom metrics dashboard was not created")
				dashPath := filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
				Expect(fileExists(dashPath)).To(BeFalse())
			})
		})
	})
})

func fileExists(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/init.go
================================================
/*
Copyright 2022 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 scaffolds

import (
	"fmt"
	log "log/slog"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates"
)

var _ plugins.Scaffolder = &initScaffolder{}

type initScaffolder struct {
	// fs is the filesystem that will be used by the scaffolder
	fs machinery.Filesystem
}

// NewInitScaffolder returns a new Scaffolder for project initialization operations
func NewInitScaffolder() plugins.Scaffolder {
	return &initScaffolder{}
}

// InjectFS implements cmdutil.Scaffolder
func (s *initScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// Scaffold implements cmdutil.Scaffolder
func (s *initScaffolder) Scaffold() error {
	log.Info("Generating Grafana manifests to visualize controller status...")

	// Initialize the machinery.Scaffold that will write the files to disk
	scaffold := machinery.NewScaffold(s.fs)

	err := scaffold.Execute(
		&templates.RuntimeManifest{},
		&templates.ResourcesManifest{},
		&templates.CustomMetricsConfigManifest{ConfigPath: configFilePath},
	)
	if err != nil {
		return fmt.Errorf("error scaffolding Grafana memanifests: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/init_test.go
================================================
//go:build integration

/*
Copyright 2025 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 scaffolds

import (
	"os"
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("Init Scaffolder", func() {
	var (
		fs         machinery.Filesystem
		scaffolder *initScaffolder
		tmpDir     string
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "grafana-init-test-*")
		Expect(err).NotTo(HaveOccurred())

		// Change to tmpDir so relative paths work correctly
		err = os.Chdir(tmpDir)
		Expect(err).NotTo(HaveOccurred())

		fs = machinery.Filesystem{
			FS: afero.NewBasePathFs(afero.NewOsFs(), tmpDir),
		}
		scaffolder = &initScaffolder{}
		scaffolder.InjectFS(fs)
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	Describe("Scaffold", func() {
		It("should handle re-scaffolding gracefully", func() {
			By("running scaffolder first time")
			err := scaffolder.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying files were created")
			runtimePath := filepath.Join("grafana", "controller-runtime-metrics.json")
			Expect(fileExistsInit(runtimePath)).To(BeTrue())

			By("running scaffolder second time")
			scaffolder2 := &initScaffolder{}
			scaffolder2.InjectFS(fs)
			err = scaffolder2.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying files still exist")
			Expect(fileExistsInit(runtimePath)).To(BeTrue())
		})
	})
})

func fileExistsInit(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom.go
================================================
/*
Copyright 2022 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 templates

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &CustomMetricsConfigManifest{}

// CustomMetricsConfigManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
type CustomMetricsConfigManifest struct {
	machinery.TemplateMixin
	ConfigPath string
}

// SetTemplateDefaults implements machinery.Template
func (f *CustomMetricsConfigManifest) SetTemplateDefaults() error {
	f.Path = f.ConfigPath

	f.TemplateBody = customMetricsConfigTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const customMetricsConfigTemplate = `---
customMetrics:
#  - metric: # Raw custom metric (required)
#    type:   # Metric type: counter/gauge/histogram (required)
#    expr:   # Prom_ql for the metric (optional)
#    unit:   # Unit of measurement, examples: s,none,bytes,percent,etc. (optional)
#
#
# Example:
# ---
# customMetrics:
#   - metric: foo_bar
#     unit: none
#     type: histogram
#   	expr: histogram_quantile(0.90, sum by(instance, le) (rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m])))
`


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/custom_metrics.go
================================================
/*
Copyright 2022 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 templates

import (
	"bytes"
	"fmt"
	"path/filepath"
	"strings"
	"text/template"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

// CustomMetricsConfig represents the configuration for custom metrics
type CustomMetricsConfig struct {
	CustomMetrics []CustomMetricItem `json:"customMetrics"`
}

// CustomMetricItem defines the config items for the custom metrics
type CustomMetricItem struct {
	Metric string `json:"metric"`
	Type   string `json:"type"`
	Expr   string `json:"expr,omitempty"`
	Unit   string `json:"unit,omitempty"`
}

var _ machinery.Template = &CustomMetricsDashManifest{}

// CustomMetricsDashManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
type CustomMetricsDashManifest struct {
	machinery.TemplateMixin

	Items []CustomMetricItem
}

// SetTemplateDefaults implements machinery.Template
func (f *CustomMetricsDashManifest) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("grafana", "custom-metrics", "custom-metrics-dashboard.json")
	}

	defaultTemplate, err := f.createTemplate()
	if err != nil {
		return err
	}

	f.TemplateBody = defaultTemplate

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

var fns = template.FuncMap{
	"plus1": func(x int) int {
		return x + 1
	},
	"hasSuffix": strings.HasSuffix,
}

func (f *CustomMetricsDashManifest) createTemplate() (string, error) {
	t := template.Must(template.New("customMetricsDashTemplate").Funcs(fns).Parse(customMetricsDashTemplate))

	outputTmpl := &bytes.Buffer{}
	if err := t.Execute(outputTmpl, f.Items); err != nil {
		return "", fmt.Errorf("error when generating manifest from config: %w", err)
	}

	return outputTmpl.String(), nil
}

const customMetricsDashTemplate = `{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "1.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "panels": [{{ $n := len . }}{{ range $i, $e := . }}
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "{{ .Unit }}"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 7,
        "w": 24
      },
      "interval": "1m",
      "links": [],
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "{{ .Expr }}",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "refId": "A",
          "step": 10
        }
      ],
      "title": "{{ .Metric }} ({{ .Type }})",
{{- if hasSuffix .Metric "_info" }}
      "transformations": [
        {
          "id": "labelsToFields",
          "options": {
            "mode": "rows"
          }
        }
      ],
      "type": "table"
{{- else }}
      "type": "timeseries"
{{- end }}
    }{{ if ne (plus1 $i) $n }},
		{{end}}{{end}}
  ],
  "refresh": "",
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "job",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "observability",
          "value": "observability"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total, namespace)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "namespace",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total, namespace)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "All",
          "value": "$__all"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
        "hide": 2,
        "includeAll": true,
        "label": "pod",
        "multi": true,
        "name": "pod",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Custom-Metrics",
  "weekStart": ""
}
`


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/resources.go
================================================
/*
Copyright 2022 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 templates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &ResourcesManifest{}

// ResourcesManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
type ResourcesManifest struct {
	machinery.TemplateMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *ResourcesManifest) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("grafana", "controller-resources-metrics.json")
	}

	// Grafana syntax use {{ }} quite often, which is collided with default delimiter for go template parsing.
	// Provide an alternative delimiter here to avoid overlaps.
	f.SetDelim("[[", "]]")
	f.TemplateBody = controllerResourcesTemplate

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const controllerResourcesTemplate = `{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "1.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "percent"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 0
      },
      "id": 2,
      "interval": "1m",
      "links": [],
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "rate(process_cpu_seconds_total{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}[5m]) * 100",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "Pod: {{pod}} | Container: {{container}}",
          "refId": "A",
          "step": 10
        }
      ],
      "title": "Controller CPU Usage",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "bytes"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 0
      },
      "id": 4,
      "interval": "1m",
      "links": [],
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "process_resident_memory_bytes{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "Pod: {{pod}} | Container: {{container}}",
          "refId": "A",
          "step": 10
        }
      ],
      "title": "Controller Memory Usage",
      "type": "timeseries"
    }
  ],
  "refresh": "",
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "job",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "observability",
          "value": "observability"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total, namespace)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "namespace",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total, namespace)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "All",
          "value": "$__all"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
        "hide": 2,
        "includeAll": true,
        "label": "pod",
        "multi": true,
        "name": "pod",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Controller-Resources-Metrics",
  "weekStart": ""
}
`


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/internal/templates/runtime.go
================================================
/*
Copyright 2022 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 templates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &RuntimeManifest{}

// RuntimeManifest scaffolds a file that defines the kustomization scheme for the prometheus folder
type RuntimeManifest struct {
	machinery.TemplateMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *RuntimeManifest) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("grafana", "controller-runtime-metrics.json")
	}

	// Grafana syntax use {{ }} quite often, which is collided with default delimiter for go template parsing.
	// Provide an alternative delimiter here to avoid overlaps.
	f.SetDelim("[[", "]]")
	f.TemplateBody = controllerRuntimeTemplate
	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

//nolint:lll
const controllerRuntimeTemplate = `{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "1.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "datasource",
          "uid": "grafana"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "collapsed": false,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 0
      },
      "id": 9,
      "panels": [],
      "title": "Reconciliation Metrics",
      "type": "row"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 3,
        "x": 0,
        "y": 1
      },
      "id": 24,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "controller_runtime_active_workers{job=\"$job\", namespace=\"$namespace\"}",
          "interval": "",
          "legendFormat": "{{controller}} {{instance}}",
          "refId": "A"
        }
      ],
      "title": "Number of workers in use",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of reconciliations per controller",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "cpm"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 11,
        "x": 3,
        "y": 1
      },
      "id": 7,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "editorMode": "code",
          "exemplar": true,
          "expr": "sum(rate(controller_runtime_reconcile_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
          "interval": "",
          "legendFormat": "{{instance}} {{pod}}",
          "range": true,
          "refId": "A"
        }
      ],
      "title": "Total Reconciliation Count Per Controller",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of reconciliation errors per controller",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "cpm"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 10,
        "x": 14,
        "y": 1
      },
      "id": 6,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "editorMode": "code",
          "exemplar": true,
          "expr": "sum(rate(controller_runtime_reconcile_errors_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
          "interval": "",
          "legendFormat": "{{instance}} {{pod}}",
          "range": true,
          "refId": "A"
        }
      ],
      "title": "Reconciliation Error Count Per Controller",
      "type": "timeseries"
    },
    {
      "collapsed": false,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 9
      },
      "id": 11,
      "panels": [],
      "title": "Work Queue Metrics",
      "type": "row"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 3,
        "x": 0,
        "y": 10
      },
      "id": 22,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "workqueue_depth{job=\"$job\", namespace=\"$namespace\"}",
          "interval": "",
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "WorkQueue Depth",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How long in seconds an item stays in workqueue before being requested",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 10,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "normal"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 11,
        "x": 3,
        "y": 10
      },
      "id": 13,
      "options": {
        "legend": {
          "calcs": [
            "max",
            "mean"
          ],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.50, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "interval": "",
          "legendFormat": "P50 {{name}} {{instance}} ",
          "refId": "A"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.90, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P90 {{name}} {{instance}} ",
          "refId": "B"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.99, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P99 {{name}} {{instance}} ",
          "refId": "C"
        }
      ],
      "title": "Seconds For Items Stay In Queue (before being requested) (P50, P90, P99)",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "ops"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 10,
        "x": 14,
        "y": 10
      },
      "id": 15,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "sum(rate(workqueue_adds_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
          "interval": "",
          "legendFormat": "{{name}} {{instance}}",
          "refId": "A"
        }
      ],
      "title": "Work Queue Add Rate",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How many seconds of work has done that is in progress and hasn't been observed by work_duration.\nLarge values indicate stuck threads.\nOne can deduce the number of stuck threads by observing the rate at which this increases.",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 3,
        "x": 0,
        "y": 18
      },
      "id": 23,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "rate(workqueue_unfinished_work_seconds{job=\"$job\", namespace=\"$namespace\"}[5m])",
          "interval": "",
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "Unfinished Seconds",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How long in seconds processing an item from workqueue takes.",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 10,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 11,
        "x": 3,
        "y": 18
      },
      "id": 19,
      "options": {
        "legend": {
          "calcs": [
            "max",
            "mean"
          ],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.50, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "interval": "",
          "legendFormat": "P50 {{name}} {{instance}} ",
          "refId": "A"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.90, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P90 {{name}} {{instance}} ",
          "refId": "B"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.99, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P99 {{name}} {{instance}} ",
          "refId": "C"
        }
      ],
      "title": "Seconds Processing Items From WorkQueue (P50, P90, P99)",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of retries handled by workqueue",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "ops"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 10,
        "x": 14,
        "y": 18
      },
      "id": 17,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "sum(rate(workqueue_retries_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
          "interval": "",
          "legendFormat": "{{name}} {{instance}} ",
          "refId": "A"
        }
      ],
      "title": "Work Queue Retries Rate",
      "type": "timeseries"
    }
  ],
  "refresh": "",
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "job",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total, namespace)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "namespace",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total, namespace)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": true,
          "text": [
            "All"
          ],
          "value": [
            "$__all"
          ]
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
        "hide": 2,
        "includeAll": true,
        "label": "pod",
        "multi": true,
        "name": "pod",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Controller-Runtime-Metrics",
  "weekStart": ""
}
`


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/scaffolds_test.go
================================================
//go:build integration

/*
Copyright 2025 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 scaffolds

import (
	"os"
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

// These tests verify the base scaffolding that both init and edit create.
// Both scaffolders generate RuntimeManifest, ResourcesManifest, and CustomMetricsConfig.
var _ = Describe("Base Scaffolds (Init & Edit)", func() {
	var (
		fs         machinery.Filesystem
		scaffolder *initScaffolder
		tmpDir     string
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "grafana-scaffolds-test-*")
		Expect(err).NotTo(HaveOccurred())

		// Change to tmpDir so relative paths work correctly
		err = os.Chdir(tmpDir)
		Expect(err).NotTo(HaveOccurred())

		fs = machinery.Filesystem{
			FS: afero.NewBasePathFs(afero.NewOsFs(), tmpDir),
		}
		scaffolder = &initScaffolder{}
		scaffolder.InjectFS(fs)
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	Context("when scaffolding grafana manifests", func() {
		It("should create controller-runtime metrics dashboard", func() {
			err := scaffolder.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying the dashboard file exists")
			runtimePath := filepath.Join("grafana", "controller-runtime-metrics.json")
			Expect(fileExistsScaffolds(runtimePath)).To(BeTrue())

			By("verifying the dashboard contains controller-runtime metrics")
			content, err := os.ReadFile(runtimePath)
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)
			Expect(contentStr).To(ContainSubstring("controller_runtime"))
			Expect(contentStr).To(ContainSubstring("reconcile"))
			Expect(contentStr).To(ContainSubstring("workqueue"))
		})

		It("should create controller-resources metrics dashboard", func() {
			err := scaffolder.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying the dashboard file exists")
			resourcesPath := filepath.Join("grafana", "controller-resources-metrics.json")
			Expect(fileExistsScaffolds(resourcesPath)).To(BeTrue())

			By("verifying the dashboard contains CPU and memory metrics")
			content, err := os.ReadFile(resourcesPath)
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)
			Expect(contentStr).To(ContainSubstring("CPU"))
			Expect(contentStr).To(ContainSubstring("Memory"))
		})

		It("should create custom metrics config template", func() {
			err := scaffolder.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying the config file exists")
			configPath := filepath.Join("grafana", "custom-metrics", "config.yaml")
			Expect(fileExistsScaffolds(configPath)).To(BeTrue())

			By("verifying the config has proper structure")
			content, err := os.ReadFile(configPath)
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)
			Expect(contentStr).To(ContainSubstring("customMetrics:"))
			Expect(contentStr).To(ContainSubstring("# Example:"))
			Expect(contentStr).To(ContainSubstring("metric:"))
			Expect(contentStr).To(ContainSubstring("type:"))
		})

		It("should create valid JSON dashboards", func() {
			err := scaffolder.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying runtime dashboard is valid JSON")
			runtimePath := filepath.Join("grafana", "controller-runtime-metrics.json")
			content, err := os.ReadFile(runtimePath)
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)
			Expect(contentStr).To(HavePrefix("{"))
			Expect(contentStr).To(HaveSuffix("}\n"))
			Expect(contentStr).To(ContainSubstring(`"__inputs"`))
			Expect(contentStr).To(ContainSubstring(`"panels"`))
		})

		It("should configure Prometheus datasource", func() {
			err := scaffolder.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying Prometheus datasource configuration")
			runtimePath := filepath.Join("grafana", "controller-runtime-metrics.json")
			content, err := os.ReadFile(runtimePath)
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)
			Expect(contentStr).To(ContainSubstring("DS_PROMETHEUS"))
			Expect(contentStr).To(ContainSubstring(`"type": "datasource"`))
			Expect(contentStr).To(ContainSubstring(`"pluginId": "prometheus"`))
		})

		It("should include template variables", func() {
			err := scaffolder.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			By("verifying template variables exist")
			runtimePath := filepath.Join("grafana", "controller-runtime-metrics.json")
			content, err := os.ReadFile(runtimePath)
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)
			Expect(contentStr).To(ContainSubstring(`"templating"`))
			Expect(contentStr).To(ContainSubstring(`"name": "namespace"`))
			Expect(contentStr).To(ContainSubstring(`"name": "job"`))
		})
	})
})

func fileExistsScaffolds(path string) bool {
	_, err := os.Stat(path)
	return err == nil
}


================================================
FILE: pkg/plugins/optional/grafana/v1alpha/scaffolds/suite_test.go
================================================
/*
Copyright 2025 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 scaffolds

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestGrafanaScaffolders(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Grafana Plugin Scaffolders Suite")
}


================================================
FILE: pkg/plugins/optional/helm/v1alpha/commons.go
================================================
/*
Copyright 2024 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 v1alpha

import (
	"errors"
	"fmt"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
)

func insertPluginMetaToConfig(target config.Config, cfg pluginConfig) error {
	key := plugin.GetPluginKeyForConfig(target.GetPluginChain(), Plugin{})
	canonicalKey := plugin.KeyFor(Plugin{})

	if err := target.DecodePluginConfig(key, &cfg); err != nil {
		switch {
		case errors.As(err, &config.UnsupportedFieldError{}):
			return nil
		case errors.As(err, &config.PluginKeyNotFoundError{}):
			if key != canonicalKey {
				if err2 := target.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
					if errors.As(err2, &config.UnsupportedFieldError{}) {
						return nil
					}
					if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
						return fmt.Errorf("error decoding plugin configuration: %w", err2)
					}
				}
			}
		default:
			return fmt.Errorf("error decoding plugin configuration: %w", err)
		}
	}

	if err := target.EncodePluginConfig(key, cfg); err != nil {
		return fmt.Errorf("error encoding plugin config: %w", err)
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/helm/v1alpha/edit.go
================================================
/*
Copyright 2024 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 v1alpha

import (
	"fmt"
	log "log/slog"
	"os"
	"path/filepath"

	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds"
)

var _ plugin.EditSubcommand = &editSubcommand{}

type editSubcommand struct {
	config config.Config
	force  bool
}

//nolint:lll
func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = `Initialize or update a Helm chart to distribute the project under the dist/ directory.

**NOTE** Before running the edit command, ensure you first execute 'make manifests' to regenerate
the latest Helm chart with your most recent changes.`

	subcmdMeta.Examples = fmt.Sprintf(`# Initialize or update a Helm chart to distribute the project under the dist/ directory
  %[1]s edit --plugins=%[2]s

# Update the Helm chart under the dist/ directory and overwrite all files
  %[1]s edit --plugins=%[2]s --force

**IMPORTANT**: If the "--force" flag is not used, the following files will not be updated to preserve your customizations:
dist/chart/
├── values.yaml
└── templates/
    └── manager/
        └── manager.yaml

The following files are never updated after their initial creation:
  - chart/Chart.yaml
  - chart/templates/_helpers.tpl
  - chart/.helmignore

All other files are updated without the usage of the '--force=true' flag
when the edit option is used to ensure that the
manifests in the chart align with the latest changes.
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
	fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
	scaffolder := scaffolds.NewHelmScaffolder(p.config, p.force)
	scaffolder.InjectFS(fs)
	err := scaffolder.Scaffold()
	if err != nil {
		return fmt.Errorf("error scaffolding Helm chart: %w", err)
	}

	// Track the resources following a declarative approach
	return insertPluginMetaToConfig(p.config, pluginConfig{})
}

// PostScaffold automatically uncomments cert-manager installation when webhooks are present
func (p *editSubcommand) PostScaffold() error {
	hasWebhooks := hasWebhooksWith(p.config)

	if hasWebhooks {
		workflowFile := filepath.Join(".github", "workflows", "test-chart.yml")
		if _, err := os.Stat(workflowFile); err != nil {
			log.Info(
				"Workflow file not found, unable to uncomment cert-manager installation",
				"error", err,
				"file", workflowFile,
			)
			return nil
		}
		//nolint:lll
		target := `
#      - name: Install cert-manager via Helm
#        run: |
#          helm repo add jetstack https://charts.jetstack.io
#          helm repo update
#          helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true
#
#      - name: Wait for cert-manager to be ready
#        run: |
#          kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager
#          kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector
#          kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook`
		if err := util.UncommentCode(workflowFile, target, "#"); err != nil {
			hasUncommented, errCheck := util.HasFileContentWith(workflowFile, "- name: Install cert-manager via Helm")
			if !hasUncommented || errCheck != nil {
				log.Warn("Failed to uncomment cert-manager installation in workflow file", "error", err, "file", workflowFile)
			}
		}
	}
	return nil
}

func hasWebhooksWith(c config.Config) bool {
	resources, err := c.GetResources()
	if err != nil {
		return false
	}

	for _, res := range resources {
		if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
			return true
		}
	}

	return false
}


================================================
FILE: pkg/plugins/optional/helm/v1alpha/plugin.go
================================================
/*
Copyright 2024 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 v1alpha

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
)

const pluginName = "helm." + plugins.DefaultNameQualifier

var (
	pluginVersion            = plugin.Version{Number: 1, Stage: stage.Alpha}
	supportedProjectVersions = []config.Version{cfgv3.Version}
)

// Plugin implements the plugin.Full interface
type Plugin struct {
	editSubcommand
}

var _ plugin.Edit = Plugin{}

type pluginConfig struct{}

// Name returns the name of the plugin
func (Plugin) Name() string { return pluginName }

// Version returns the version of the Helm plugin
func (Plugin) Version() plugin.Version { return pluginVersion }

// SupportedProjectVersions returns an array with all project versions supported by the plugin
func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }

// GetEditSubcommand will return the subcommand which is responsible for adding and/or edit a helm chart
func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }

// Description returns a short description of the plugin
func (Plugin) Description() string {
	return "Generate Helm Chart (deprecated, use v2-alpha)"
}

// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
func (p Plugin) DeprecationWarning() string {
	return "helm/v1-alpha plugin is deprecated, use helm/v2-alpha instead which " +
		"provides dynamic Helm chart generation from kustomize output"
}


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/edit.go
================================================
/*
Copyright 2024 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 scaffolds

import (
	"errors"
	"fmt"
	log "log/slog"
	"os"
	"path/filepath"
	"regexp"
	"strings"

	"sigs.k8s.io/yaml"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates"
	charttemplates "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates"
	templatescertmanager "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager"
	templatesmetrics "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus"
	templateswebhooks "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github"
)

var _ plugins.Scaffolder = &editScaffolder{}

type editScaffolder struct {
	config config.Config

	fs machinery.Filesystem

	force bool
}

// NewHelmScaffolder returns a new Scaffolder for HelmPlugin
func NewHelmScaffolder(cfg config.Config, force bool) plugins.Scaffolder {
	return &editScaffolder{
		config: cfg,
		force:  force,
	}
}

// InjectFS implements cmdutil.Scaffolder
func (s *editScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// Scaffold scaffolds the Helm chart with the necessary files.
func (s *editScaffolder) Scaffold() error {
	log.Info("Generating Helm Chart to distribute project")

	imagesEnvVars := s.getDeployImagesEnvVars()

	scaffold := machinery.NewScaffold(s.fs,
		machinery.WithConfig(s.config),
	)

	// Found webhooks by looking at the config our scaffolds files
	mutatingWebhooks, validatingWebhooks, err := s.extractWebhooksFromGeneratedFiles()
	if err != nil {
		return fmt.Errorf("failed to extract webhooks: %w", err)
	}
	hasWebhooks := hasWebhooksWith(s.config) || (len(mutatingWebhooks) > 0 && len(validatingWebhooks) > 0)

	buildScaffold := []machinery.Builder{
		&github.HelmChartCI{},
		&templates.HelmChart{},
		&templates.HelmValues{
			HasWebhooks:  hasWebhooks,
			DeployImages: imagesEnvVars,
			Force:        s.force,
		},
		&templates.HelmIgnore{},
		&charttemplates.HelmHelpers{},
		&manager.Deployment{
			Force:        s.force,
			DeployImages: len(imagesEnvVars) > 0,
			HasWebhooks:  hasWebhooks,
		},
		&templatescertmanager.Certificate{HasWebhooks: hasWebhooks},
		&templatesmetrics.Service{},
		&prometheus.Monitor{},
	}

	if len(mutatingWebhooks) > 0 || len(validatingWebhooks) > 0 {
		buildScaffold = append(buildScaffold,
			&templateswebhooks.Template{
				MutatingWebhooks:   mutatingWebhooks,
				ValidatingWebhooks: validatingWebhooks,
			},
		)
	}

	if hasWebhooks {
		buildScaffold = append(buildScaffold,
			&templateswebhooks.Service{},
		)
	}

	if err = scaffold.Execute(buildScaffold...); err != nil {
		return fmt.Errorf("error scaffolding helm-chart manifests: %w", err)
	}

	// Copy relevant files from config/ to dist/chart/templates/
	err = s.copyConfigFiles()
	if err != nil {
		return fmt.Errorf("failed to copy manifests from config to dist/chart/templates/: %w", err)
	}

	return nil
}

// getDeployImagesEnvVars will return the values to append the envvars for projects
// which has the APIs scaffolded with DeployImage plugin
func (s *editScaffolder) getDeployImagesEnvVars() map[string]string {
	deployImages := make(map[string]string)

	pluginConfig := struct {
		Resources []struct {
			Kind    string            `json:"kind"`
			Options map[string]string `json:"options"`
		} `json:"resources"`
	}{}

	key := plugin.GetPluginKeyForConfig(s.config.GetPluginChain(), deployimagev1alpha1.Plugin{})
	canonicalKey := plugin.KeyFor(deployimagev1alpha1.Plugin{})

	err := s.config.DecodePluginConfig(key, &pluginConfig)
	if err != nil {
		switch {
		case errors.As(err, &config.UnsupportedFieldError{}):
			return deployImages
		case errors.As(err, &config.PluginKeyNotFoundError{}):
			if key != canonicalKey {
				if err2 := s.config.DecodePluginConfig(canonicalKey, &pluginConfig); err2 != nil {
					if errors.As(err2, &config.UnsupportedFieldError{}) {
						return deployImages
					}
					if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
						log.Warn("error decoding deploy-image configuration", "error", err2, "key", canonicalKey)
					}
				}
			}
		default:
			log.Warn("error decoding deploy-image configuration", "error", err, "key", key)
		}
	}

	for _, res := range pluginConfig.Resources {
		image, ok := res.Options["image"]
		if ok {
			deployImages[strings.ToUpper(res.Kind)] = image
		}
	}
	return deployImages
}

// extractWebhooksFromGeneratedFiles parses the files generated by controller-gen under
// config/webhooks and created Mutating and Validating helper structures to
// generate the webhook manifest for the helm-chart
func (s *editScaffolder) extractWebhooksFromGeneratedFiles() (mutatingWebhooks []templateswebhooks.DataWebhook,
	validatingWebhooks []templateswebhooks.DataWebhook, err error,
) {
	manifestFile := "config/webhook/manifests.yaml"

	if _, err = os.Stat(manifestFile); os.IsNotExist(err) {
		log.Info("webhook manifests were not found", "path", manifestFile)
		return nil, nil, nil
	}

	content, err := os.ReadFile(manifestFile)
	if err != nil {
		return nil, nil,
			fmt.Errorf("failed to read %q: %w", manifestFile, err)
	}

	docs := strings.SplitSeq(string(content), "---")
	for doc := range docs {
		var webhookConfig struct {
			Kind     string `yaml:"kind"`
			Webhooks []struct {
				Name         string `yaml:"name"`
				ClientConfig struct {
					Service struct {
						Name      string `yaml:"name"`
						Namespace string `yaml:"namespace"`
						Path      string `yaml:"path"`
					} `yaml:"service"`
				} `yaml:"clientConfig"`
				Rules                   []templateswebhooks.DataWebhookRule `yaml:"rules"`
				FailurePolicy           string                              `yaml:"failurePolicy"`
				SideEffects             string                              `yaml:"sideEffects"`
				AdmissionReviewVersions []string                            `yaml:"admissionReviewVersions"`
			} `yaml:"webhooks"`
		}

		if err := yaml.Unmarshal([]byte(doc), &webhookConfig); err != nil {
			log.Error("failed to unmarshal webhook YAML", "error", err)
			continue
		}

		for _, w := range webhookConfig.Webhooks {
			for i := range w.Rules {
				if len(w.Rules[i].APIGroups) == 0 {
					w.Rules[i].APIGroups = []string{""}
				}
			}
			webhook := templateswebhooks.DataWebhook{
				Name:                    w.Name,
				ServiceName:             fmt.Sprintf("%s-webhook-service", s.config.GetProjectName()),
				Path:                    w.ClientConfig.Service.Path,
				FailurePolicy:           w.FailurePolicy,
				SideEffects:             w.SideEffects,
				AdmissionReviewVersions: w.AdmissionReviewVersions,
				Rules:                   w.Rules,
			}

			switch webhookConfig.Kind {
			case "MutatingWebhookConfiguration":
				mutatingWebhooks = append(mutatingWebhooks, webhook)
			case "ValidatingWebhookConfiguration":
				validatingWebhooks = append(validatingWebhooks, webhook)
			}
		}
	}

	return mutatingWebhooks, validatingWebhooks, nil
}

// Helper function to copy files from config/ to dist/chart/templates/
func (s *editScaffolder) copyConfigFiles() error {
	configDirs := []struct {
		SrcDir  string
		DestDir string
		SubDir  string
	}{
		{"config/rbac", "dist/chart/templates/rbac", "rbac"},
		{"config/crd/bases", "dist/chart/templates/crd", "crd"},
		{"config/network-policy", "dist/chart/templates/network-policy", "networkPolicy"},
	}

	for _, dir := range configDirs {
		// Check if the source directory exists
		if _, err := os.Stat(dir.SrcDir); os.IsNotExist(err) {
			// Skip if the source directory does not exist
			continue
		}

		files, err := filepath.Glob(filepath.Join(dir.SrcDir, "*.yaml"))
		if err != nil {
			return fmt.Errorf("failed finding files in %q: %w", dir.SrcDir, err)
		}

		// Skip processing if the directory is empty (no matching files)
		if len(files) == 0 {
			continue
		}

		// Ensure destination directory exists
		if err := os.MkdirAll(dir.DestDir, 0o755); err != nil {
			return fmt.Errorf("failed to create directory %q: %w", dir.DestDir, err)
		}

		for _, srcFile := range files {
			destFile := filepath.Join(dir.DestDir, filepath.Base(srcFile))

			hasConvertionalWebhook := false
			if hasWebhooksWith(s.config) {
				resources, err := s.config.GetResources()
				if err != nil {
					break
				}
				for _, res := range resources {
					if res.HasConversionWebhook() {
						hasConvertionalWebhook = true
						break
					}
				}
			}

			err := copyFileWithHelmLogic(srcFile, destFile, dir.SubDir, s.config.GetProjectName(), hasConvertionalWebhook)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

// copyFileWithHelmLogic reads the source file, modifies the content for Helm, applies patches
// to spec.conversion if applicable, and writes it to the destination
func copyFileWithHelmLogic(srcFile, destFile, subDir, projectName string, hasConvertionalWebhook bool) error {
	if _, err := os.Stat(srcFile); os.IsNotExist(err) {
		log.Info("Source file does not exist", "source_file", srcFile)
		return fmt.Errorf("source file does not exist %q: %w", srcFile, err)
	}

	content, err := os.ReadFile(srcFile)
	if err != nil {
		log.Info("Error reading source file", "source_file", srcFile)
		return fmt.Errorf("failed to read file %q: %w", srcFile, err)
	}

	contentStr := string(content)

	// Skip kustomization.yaml or kustomizeconfig.yaml files
	if strings.HasSuffix(srcFile, "kustomization.yaml") ||
		strings.HasSuffix(srcFile, "kustomizeconfig.yaml") {
		return nil
	}

	// Apply RBAC-specific replacements
	if subDir == "rbac" {
		contentStr = strings.ReplaceAll(contentStr,
			"name: controller-manager",
			"name: {{ .Values.controllerManager.serviceAccountName }}")
		contentStr = strings.Replace(contentStr,
			"name: metrics-reader",
			fmt.Sprintf("name: %s-metrics-reader", projectName), 1)

		contentStr = strings.ReplaceAll(contentStr,
			"name: metrics-auth-role",
			fmt.Sprintf("name: %s-metrics-auth-role", projectName))
		contentStr = strings.Replace(contentStr,
			"name: metrics-auth-rolebinding",
			fmt.Sprintf("name: %s-metrics-auth-rolebinding", projectName), 1)

		if strings.Contains(contentStr, ".Values.controllerManager.serviceAccountName") &&
			strings.Contains(contentStr, "kind: ServiceAccount") &&
			!strings.Contains(contentStr, "RoleBinding") {
			// The generated Service Account does not have the annotations field so we must add it.
			contentStr = strings.Replace(contentStr,
				"metadata:", `metadata:
  {{- if and .Values.controllerManager.serviceAccount .Values.controllerManager.serviceAccount.annotations }}
  annotations:
    {{- range $key, $value := .Values.controllerManager.serviceAccount.annotations }}
    {{ $key }}: {{ $value }}
    {{- end }}
  {{- end }}`, 1)
		}
		contentStr = strings.ReplaceAll(contentStr,
			"name: leader-election-role",
			fmt.Sprintf("name: %s-leader-election-role", projectName))
		contentStr = strings.Replace(contentStr,
			"name: leader-election-rolebinding",
			fmt.Sprintf("name: %s-leader-election-rolebinding", projectName), 1)
		contentStr = strings.ReplaceAll(contentStr,
			"name: manager-role",
			fmt.Sprintf("name: %s-manager-role", projectName))
		contentStr = strings.Replace(contentStr,
			"name: manager-rolebinding",
			fmt.Sprintf("name: %s-manager-rolebinding", projectName), 1)

		// The generated files do not include the namespace
		if strings.Contains(contentStr, "leader-election-rolebinding") ||
			strings.Contains(contentStr, "leader-election-role") {
			namespace := `
  namespace: {{ .Release.Namespace }}`
			contentStr = strings.Replace(contentStr, "metadata:", "metadata:"+namespace, 1)
		}
	}

	// Conditionally handle CRD patches and annotations for CRDs
	if subDir == "crd" {
		kind, group := extractKindAndGroupFromFileName(filepath.Base(srcFile))
		hasWebhookPatch := false

		// Retrieve patch content for the CRD's spec.conversion, if it exists
		patchContent, patchExists, errPatch := getCRDPatchContent(kind, group)
		if errPatch != nil {
			return errPatch
		}

		// If patch content exists, inject it under spec.conversion with Helm conditional
		if patchExists {
			conversionSpec := extractConversionSpec(patchContent)
			// Projects scaffolded with old Kubebuilder versions does not have the conversion
			// webhook properly generated because before 4.4.0 this feature was not fully addressed.
			// The patch was added by default when should not. See the related fixes:
			//
			// Issue fixed in release 4.3.1: (which will cause the injection of webhook conditionals for projects without
			// conversion webhooks)
			// (kustomize/v2, go/v4): Corrected the generation of manifests under config/crd/patches
			// to ensure the /convert service patch is only created for webhooks configured with --conversion. (#4280)
			//
			// Conversion webhook fully fixed in release 4.4.0:
			// (kustomize/v2, go/v4): Fixed CA injection for conversion webhooks. Previously, the CA injection
			// was applied incorrectly to all CRDs instead of only conversion types. The issue dates back to release 3.5.0
			// due to kustomize/v2-alpha changes. Now, conversion webhooks are properly generated. (#4254, #4282)
			if len(conversionSpec) > 0 && !hasConvertionalWebhook {
				log.Warn("\n" +
					"============================================================\n" +
					"| [WARNING] Webhook Patch Issue Detected                   |\n" +
					"============================================================\n" +
					"Webhook patch found, but no conversion webhook is configured for this project.\n\n" +
					"Note: Older scaffolds have an issue where the conversion webhook patch was \n" +
					"      scaffolded by default, and conversion webhook injection was not properly limited \n" +
					"      to specific CRDs.\n\n" +
					"Recommended Action:\n" +
					"   - Upgrade your project to the latest available version.\n" +
					"   - Consider using the 'alpha generate' command.\n\n" +
					"The cert-manager injection and webhook conversion patch found for CRDs will\n" +
					"be skipped and NOT added to the Helm chart.\n" +
					"============================================================")

				hasWebhookPatch = false
			} else {
				contentStr = injectConversionSpecWithCondition(contentStr, conversionSpec)
				hasWebhookPatch = true
			}
		}

		// Inject annotations after "annotations:" in a single block without extra spaces
		contentStr = injectAnnotations(contentStr, hasWebhookPatch)
	}

	// Remove existing labels if necessary
	contentStr = removeLabels(contentStr)

	// Replace namespace with Helm template variable
	contentStr = strings.ReplaceAll(contentStr, "namespace: system", "namespace: {{ .Release.Namespace }}")

	contentStr = strings.Replace(contentStr, "metadata:", `metadata:
  labels:
    {{- include "chart.labels" . | nindent 4 }}`, 1)

	// Append project name to webhook service name
	contentStr = strings.ReplaceAll(contentStr, "name: webhook-service", "name: "+projectName+"-webhook-service")

	var wrappedContent string
	if isMetricRBACFile(subDir, srcFile) {
		wrappedContent = fmt.Sprintf(
			"{{- if and .Values.rbac.enable .Values.metrics.enable }}\n%s{{- end -}}\n", contentStr)
	} else {
		wrappedContent = fmt.Sprintf(
			"{{- if .Values.%s.enable }}\n%s{{- end -}}\n", subDir, contentStr)
	}

	if err = os.MkdirAll(filepath.Dir(destFile), 0o755); err != nil {
		return fmt.Errorf("error creating directory %q: %w", filepath.Dir(destFile), err)
	}

	err = os.WriteFile(destFile, []byte(wrappedContent), 0o644)
	if err != nil {
		log.Info("Error writing destination file", "destination_file", destFile)
		return fmt.Errorf("error writing destination file %q: %w", destFile, err)
	}

	log.Info("Successfully copied file", "from", srcFile, "to", destFile)
	return nil
}

// extractKindAndGroupFromFileName extracts the kind and group from a CRD filename
func extractKindAndGroupFromFileName(fileName string) (kind, group string) {
	parts := strings.Split(fileName, "_")
	if len(parts) >= 2 {
		group = strings.Split(parts[0], ".")[0] // Extract group up to the first dot
		kind = strings.TrimSuffix(parts[1], ".yaml")
	}
	return kind, group
}

// getCRDPatchContent finds and reads the appropriate patch content for a given kind and group
func getCRDPatchContent(kind, group string) (string, bool, error) {
	// First, look for patches that contain both "webhook", the group, and kind in their filename
	groupKindPattern := fmt.Sprintf("config/crd/patches/webhook_*%s*%s*.yaml", group, kind)
	patchFiles, err := filepath.Glob(groupKindPattern)
	if err != nil {
		return "", false, fmt.Errorf("failed to list patches: %w", err)
	}

	// If no group-specific patch found, search for patches that contain only "webhook" and the kind
	if len(patchFiles) == 0 {
		kindOnlyPattern := fmt.Sprintf("config/crd/patches/webhook_*%s*.yaml", kind)
		patchFiles, err = filepath.Glob(kindOnlyPattern)
		if err != nil {
			return "", false, fmt.Errorf("failed to list patches: %w", err)
		}
	}

	// Read the first matching patch file (if any)
	if len(patchFiles) > 0 {
		patchContent, err := os.ReadFile(patchFiles[0])
		if err != nil {
			return "", false, fmt.Errorf("failed to read patch file %q: %w", patchFiles[0], err)
		}
		return string(patchContent), true, nil
	}

	return "", false, nil
}

// extractConversionSpec extracts only the conversion section from the patch content
func extractConversionSpec(patchContent string) string {
	specStart := strings.Index(patchContent, "conversion:")
	if specStart == -1 {
		return ""
	}
	return patchContent[specStart:]
}

// injectConversionSpecWithCondition inserts the conversion spec under the main spec field with Helm conditional
func injectConversionSpecWithCondition(contentStr, conversionSpec string) string {
	specPosition := strings.Index(contentStr, "spec:")
	if specPosition == -1 {
		return contentStr // No spec field found; return unchanged
	}
	conditionalSpec := fmt.Sprintf("\n  {{- if .Values.webhook.enable }}\n  %s\n  {{- end }}",
		strings.TrimRight(conversionSpec, "\n"))
	return contentStr[:specPosition+5] + conditionalSpec + contentStr[specPosition+5:]
}

// injectAnnotations inserts the required annotations after the "annotations:" field in a single block without
// extra spaces
func injectAnnotations(contentStr string, hasWebhookPatch bool) string {
	annotationsBlock := `
    {{- if .Values.certmanager.enable }}
    cert-manager.io/inject-ca-from: "{{ .Release.Namespace }}/serving-cert"
    {{- end }}
    {{- if .Values.crd.keep }}
    "helm.sh/resource-policy": keep
    {{- end }}`
	if hasWebhookPatch {
		return strings.Replace(contentStr, "annotations:", "annotations:"+annotationsBlock, 1)
	}

	// Apply only resource policy if no webhook patch
	resourcePolicy := `
    {{- if .Values.crd.keep }}
    "helm.sh/resource-policy": keep
    {{- end }}`
	return strings.Replace(contentStr, "annotations:", "annotations:"+resourcePolicy, 1)
}

// isMetricRBACFile checks if the file is in the "rbac"
// subdirectory and matches one of the metric-related RBAC filenames
func isMetricRBACFile(subDir, srcFile string) bool {
	return subDir == "rbac" && (strings.HasSuffix(srcFile, "metrics_auth_role.yaml") ||
		strings.HasSuffix(srcFile, "metrics_auth_role_binding.yaml") ||
		strings.HasSuffix(srcFile, "metrics_reader_role.yaml"))
}

// removeLabels removes any existing labels section from the content
func removeLabels(content string) string {
	labelRegex := `(?m)^  labels:\n(?:    [^\n]+\n)*`
	re := regexp.MustCompile(labelRegex)

	return re.ReplaceAllString(content, "")
}

func hasWebhooksWith(c config.Config) bool {
	// Get the list of resources
	resources, err := c.GetResources()
	if err != nil {
		return false // If there's an error getting resources, assume no webhooks
	}

	for _, res := range resources {
		if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
			return true
		}
	}

	return false
}


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/cert-manager/certificate.go
================================================
/*
Copyright 2024 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 webhook

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Certificate{}

// Certificate scaffolds the Certificate for webhooks in the Helm chart
type Certificate struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// HasWebhooks is true when webhooks were found in the config
	HasWebhooks bool
}

// SetTemplateDefaults sets the default template configuration
func (f *Certificate) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "templates", "certmanager", "certificate.yaml")
	}

	f.TemplateBody = certificateTemplate

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const certificateTemplate = `{{` + "`" + `{{- if .Values.certmanager.enable }}` + "`" + `}}
# Self-signed Issuer
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
  name: selfsigned-issuer
  namespace: {{ "{{ .Release.Namespace }}" }}
spec:
  selfSigned: {}
{{- if .HasWebhooks }}
{{ "{{- if .Values.webhook.enable }}" }}
---
# Certificate for the webhook
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  annotations:
    {{ "{{- if .Values.crd.keep }}" }}
    "helm.sh/resource-policy": keep
    {{ "{{- end }}" }}
  name: serving-cert
  namespace: {{ "{{ .Release.Namespace }}" }}
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
spec:
  dnsNames:
    - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc
    - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc.cluster.local
    - {{ .ProjectName }}-webhook-service.{{ "{{ .Release.Namespace }}" }}.svc
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: webhook-server-cert
{{` + "`" + `{{- end }}` + "`" + `}}
{{- end }}
{{ "{{- if .Values.metrics.enable }}" }}
---
# Certificate for the metrics
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  annotations:
    {{ "{{- if .Values.crd.keep }}" }}
    "helm.sh/resource-policy": keep
    {{ "{{- end }}" }}
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
  name: metrics-certs
  namespace: {{ "{{ .Release.Namespace }}" }}
spec:
  dnsNames:
    - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc
    - {{ .ProjectName }}.{{ "{{ .Release.Namespace }}" }}.svc.cluster.local
    - {{ .ProjectName }}-metrics-service.{{ "{{ .Release.Namespace }}" }}.svc
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: metrics-server-cert
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go
================================================
/*
Copyright 2024 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 charttemplates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmHelpers{}

// HelmHelpers scaffolds the _helpers.tpl file for Helm charts
type HelmHelpers struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults sets the default template configuration
func (f *HelmHelpers) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "templates", "_helpers.tpl")
	}

	f.TemplateBody = helmHelpersTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const helmHelpersTemplate = `{{` + "`" + `{{- define "chart.name" -}}` + "`" + `}}
{{` + "`" + `{{- if .Chart }}` + "`" + `}}
  {{` + "`" + `{{- if .Chart.Name }}` + "`" + `}}
    {{` + "`" + `{{- .Chart.Name | trunc 63 | trimSuffix "-" }}` + "`" + `}}
  {{` + "`" + `{{- else if .Values.nameOverride }}` + "`" + `}}
    {{` + "`" + `{{ .Values.nameOverride | trunc 63 | trimSuffix "-" }}` + "`" + `}}
  {{` + "`" + `{{- else }}` + "`" + `}}
    {{ .ProjectName }}
  {{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{- else }}` + "`" + `}}
  {{ .ProjectName }}
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}

{{/*
Common labels for the chart.
*/}}
{{` + "`" + `{{- define "chart.labels" -}}` + "`" + `}}
{{` + "`" + `{{- if .Chart.AppVersion -}}` + "`" + `}}
app.kubernetes.io/version: {{` + "`" + `{{ .Chart.AppVersion | quote }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{- if .Chart.Version }}` + "`" + `}}
helm.sh/chart: {{` + "`" + `{{ .Chart.Version | quote }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
app.kubernetes.io/name: {{` + "`" + `{{ include "chart.name" . }}` + "`" + `}}
app.kubernetes.io/instance: {{` + "`" + `{{ .Release.Name }}` + "`" + `}}
app.kubernetes.io/managed-by: {{` + "`" + `{{ .Release.Service }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}

{{/*
Selector labels for the chart.
*/}}
{{` + "`" + `{{- define "chart.selectorLabels" -}}` + "`" + `}}
app.kubernetes.io/name: {{` + "`" + `{{ include "chart.name" . }}` + "`" + `}}
app.kubernetes.io/instance: {{` + "`" + `{{ .Release.Name }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}

{{/*
Helper to check if mutating webhooks exist in the services.
*/}}
{{` + "`" + `{{- define "chart.hasMutatingWebhooks" -}}` + "`" + `}}
{{` + "`" + `{{- $hasMutating := false }}` + "`" + `}}
{{` + "`" + `{{- range . }}` + "`" + `}}
  {{` + "`" + `{{- if eq .type "mutating" }}` + "`" + `}}
    {{` + "`" + `$hasMutating = true }}{{- end }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{ $hasMutating }}}}{{- end }}` + "`" + `}}

{{/*
Helper to check if validating webhooks exist in the services.
*/}}
{{` + "`" + `{{- define "chart.hasValidatingWebhooks" -}}` + "`" + `}}
{{` + "`" + `{{- $hasValidating := false }}` + "`" + `}}
{{` + "`" + `{{- range . }}` + "`" + `}}
  {{` + "`" + `{{- if eq .type "validating" }}` + "`" + `}}
    {{` + "`" + `$hasValidating = true }}{{- end }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{ $hasValidating }}}}{{- end }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/manager/manager.go
================================================
/*
Copyright 2024 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 manager

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Deployment{}

// Deployment scaffolds the manager Deployment for the Helm chart
type Deployment struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// DeployImages if true will scaffold the env with the images
	DeployImages bool
	// Force if true allow overwrite the scaffolded file
	Force bool
	// HasWebhooks is true when webhooks were found in the config
	HasWebhooks bool
}

// SetTemplateDefaults sets the default template configuration
func (f *Deployment) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "templates", "manager", "manager.yaml")
	}

	f.TemplateBody = managerDeploymentTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

//nolint:lll
const managerDeploymentTemplate = `apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .ProjectName }}-controller-manager
  namespace: {{ "{{ .Release.Namespace }}" }}
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
    control-plane: controller-manager
spec:
  replicas:  {{ "{{ .Values.controllerManager.replicas }}" }}
  selector:
    matchLabels:
      {{ "{{- include \"chart.selectorLabels\" . | nindent 6 }}" }}
      control-plane: controller-manager
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        {{ "{{- include \"chart.labels\" . | nindent 8 }}" }}
        control-plane: controller-manager
        {{ "{{- if and .Values.controllerManager.pod .Values.controllerManager.pod.labels }}" }}
        {{ "{{- range $key, $value := .Values.controllerManager.pod.labels }}" }}
        {{ "{{ $key }}" }}: {{ "{{ $value }}" }}
        {{ "{{- end }}" }}
        {{ "{{- end }}" }}
    spec:
      containers:
        - name: manager
          args:
            {{ "{{- range .Values.controllerManager.container.args }}" }}
            - {{ "{{ . }}" }}
            {{ "{{- end }}" }}
          command:
            - /manager
          image: {{ "{{ .Values.controllerManager.container.image.repository }}" }}:{{ "{{ .Values.controllerManager.container.image.tag }}" }}
          {{ "{{- if .Values.controllerManager.container.imagePullPolicy }}" }}
          imagePullPolicy: {{ "{{ .Values.controllerManager.container.imagePullPolicy }}" }}
          {{ "{{- end }}" }}
          {{ "{{- if .Values.controllerManager.container.env }}" }}
          env:
            {{ "{{- range $key, $value := .Values.controllerManager.container.env }}" }}
            - name: {{ "{{ $key }}" }}
              value: {{ "{{ $value }}" }}
            {{ "{{- end }}" }}
          {{ "{{- end }}" }}
          livenessProbe:
            {{ "{{- toYaml .Values.controllerManager.container.livenessProbe | nindent 12 }}" }}
          readinessProbe:
            {{ "{{- toYaml .Values.controllerManager.container.readinessProbe | nindent 12 }}" }}
{{- if .HasWebhooks }}
          {{ "{{- if .Values.webhook.enable }}" }}
          ports:
            - containerPort: 9443
              name: webhook-server
              protocol: TCP
          {{ "{{- end }}" }}
{{- end }}
          resources:
            {{ "{{- toYaml .Values.controllerManager.container.resources | nindent 12 }}" }}
          securityContext:
            {{ "{{- toYaml .Values.controllerManager.container.securityContext | nindent 12 }}" }}
{{- if .HasWebhooks }}
          {{ "{{- if and .Values.certmanager.enable (or .Values.webhook.enable .Values.metrics.enable) }}" }}
{{- else }}
          {{ "{{- if and .Values.certmanager.enable .Values.metrics.enable }}" }}
{{- end }}
          volumeMounts:
{{- if .HasWebhooks }}
            {{ "{{- if and .Values.webhook.enable .Values.certmanager.enable }}" }}
            - name: webhook-cert
              mountPath: /tmp/k8s-webhook-server/serving-certs
              readOnly: true
            {{ "{{- end }}" }}
{{- end }}
            {{ "{{- if and .Values.metrics.enable .Values.certmanager.enable }}" }}
            - name: metrics-certs
              mountPath: /tmp/k8s-metrics-server/metrics-certs
              readOnly: true
            {{ "{{- end }}" }}
          {{ "{{- end }}" }}
      securityContext:
        {{ "{{- toYaml .Values.controllerManager.securityContext | nindent 8 }}" }}
      serviceAccountName: {{ "{{ .Values.controllerManager.serviceAccountName }}" }}
      terminationGracePeriodSeconds: {{ "{{ .Values.controllerManager.terminationGracePeriodSeconds }}" }}
{{- if .HasWebhooks }}
      {{ "{{- if and .Values.certmanager.enable (or .Values.webhook.enable .Values.metrics.enable) }}" }}
{{- else }}
      {{ "{{- if and .Values.certmanager.enable .Values.metrics.enable }}" }}
{{- end }}
      volumes:
{{- if .HasWebhooks }}
        {{ "{{- if and .Values.webhook.enable .Values.certmanager.enable }}" }}
        - name: webhook-cert
          secret:
            secretName: webhook-server-cert
        {{ "{{- end }}" }}
{{- end }}
        {{ "{{- if and .Values.metrics.enable .Values.certmanager.enable }}" }}
        - name: metrics-certs
          secret:
            secretName: metrics-server-cert
        {{ "{{- end }}" }}
      {{ "{{- end }}" }}
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/metrics/metrics_service.go
================================================
/*
Copyright 2024 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 metrics

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Service{}

// Service scaffolds the Service for metrics in the Helm chart
type Service struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults sets the default template configuration
func (f *Service) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "templates", "metrics", "metrics-service.yaml")
	}

	f.TemplateBody = metricsServiceTemplate

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const metricsServiceTemplate = `{{` + "`" + `{{- if .Values.metrics.enable }}` + "`" + `}}
apiVersion: v1
kind: Service
metadata:
  name: {{ .ProjectName }}-controller-manager-metrics-service
  namespace: {{ "{{ .Release.Namespace }}" }}
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
    control-plane: controller-manager
spec:
  ports:
    - port: 8443
      targetPort: 8443
      protocol: TCP
      name: https
  selector:
    control-plane: controller-manager
{{` + "`" + `{{- end }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/prometheus/monitor.go
================================================
/*
Copyright 2024 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 prometheus

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Monitor{}

// Monitor scaffolds the ServiceMonitor for Prometheus in the Helm chart
type Monitor struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults sets the default template configuration
func (f *Monitor) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "templates", "prometheus", "monitor.yaml")
	}

	f.TemplateBody = monitorTemplate

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const monitorTemplate = `# To integrate with Prometheus.
{{ "{{- if .Values.prometheus.enable }}" }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
    control-plane: controller-manager
  name: {{ .ProjectName }}-controller-manager-metrics-monitor
  namespace: {{ "{{ .Release.Namespace }}" }}
spec:
  endpoints:
    - path: /metrics
      port: https
      scheme: https
      bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
      tlsConfig:
        {{ "{{- if .Values.certmanager.enable }}" }}
        serverName: {{ .ProjectName }}-controller-manager-metrics-service.{{ "{{ .Release.Namespace }}" }}.svc
        # Apply secure TLS configuration with cert-manager
        insecureSkipVerify: false
        ca:
          secret:
            name: metrics-server-cert
            key: ca.crt
        cert:
          secret:
            name: metrics-server-cert
            key: tls.crt
        keySecret:
          name: metrics-server-cert
          key: tls.key
        {{ "{{- else }}" }}
        # Development/Test mode (insecure configuration)
        insecureSkipVerify: true
        {{ "{{- end }}" }}
  selector:
    matchLabels:
      control-plane: controller-manager
{{ "{{- end }}" }}
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/service.go
================================================
/*
Copyright 2024 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 webhook

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Service{}

// Service scaffolds the Service for webhooks in the Helm chart
type Service struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// Force if true allows overwriting the scaffolded file
	Force bool
}

// SetTemplateDefaults sets the default template configuration
func (f *Service) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "templates", "webhook", "service.yaml")
	}

	f.TemplateBody = webhookServiceTemplate

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const webhookServiceTemplate = `{{` + "`" + `{{- if .Values.webhook.enable }}` + "`" + `}}
apiVersion: v1
kind: Service
metadata:
  name: {{ .ProjectName }}-webhook-service
  namespace: {{ "{{ .Release.Namespace }}" }}
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
spec:
  ports:
    - port: 443
      protocol: TCP
      targetPort: 9443
  selector:
    control-plane: controller-manager
{{` + "`" + `{{- end }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart-templates/webhook/webhook.go
================================================
/*
Copyright 2024 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 webhook

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Template{}

// Template scaffolds both MutatingWebhookConfiguration and ValidatingWebhookConfiguration for the Helm chart
type Template struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	MutatingWebhooks   []DataWebhook
	ValidatingWebhooks []DataWebhook
}

// SetTemplateDefaults sets default configuration for the webhook template
func (f *Template) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "templates", "webhook", "webhooks.yaml")
	}

	f.TemplateBody = webhookTemplate
	f.IfExistsAction = machinery.OverwriteFile
	return nil
}

// DataWebhook helps generate manifests based on the data gathered from the kustomize files
type DataWebhook struct {
	ServiceName             string
	Name                    string
	Path                    string
	Type                    string
	FailurePolicy           string
	SideEffects             string
	AdmissionReviewVersions []string
	Rules                   []DataWebhookRule
}

// DataWebhookRule helps generate manifests based on the data gathered from the kustomize files
type DataWebhookRule struct {
	Operations  []string
	APIGroups   []string
	APIVersions []string
	Resources   []string
}

const webhookTemplate = `{{` + "`" + `{{- if .Values.webhook.enable }}` + "`" + `}}

{{- if .MutatingWebhooks }}
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: {{ .ProjectName }}-mutating-webhook-configuration
  namespace: {{ "{{ .Release.Namespace }}" }}
  annotations:
    {{` + "`" + `{{- if .Values.certmanager.enable }}` + "`" + `}}
    cert-manager.io/inject-ca-from: "{{` + "`" + `{{ $.Release.Namespace }}` + "`" + `}}/serving-cert"
    {{` + "`" + `{{- end }}` + "`" + `}}
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
webhooks:
  {{- range .MutatingWebhooks }}
  - name: {{ .Name }}
    clientConfig:
      service:
        name: {{ .ServiceName }}
        namespace: {{ "{{ .Release.Namespace }}" }}
        path: {{ .Path }}
    failurePolicy: {{ .FailurePolicy }}
    sideEffects: {{ .SideEffects }}
    admissionReviewVersions:
      {{- range .AdmissionReviewVersions }}
      - {{ . }}
      {{- end }}
    rules:
      {{- range .Rules }}
      - operations:
          {{- range .Operations }}
          - {{ . }}
          {{- end }}
        apiGroups:
          {{- range .APIGroups }}
          - {{ . }}
          {{- end }}
        apiVersions:
          {{- range .APIVersions }}
          - {{ . }}
          {{- end }}
        resources:
          {{- range .Resources }}
          - {{ . }}
          {{- end }}
      {{- end -}}
  {{- end }}
{{- end }}
{{- if and .MutatingWebhooks .ValidatingWebhooks }}
---
{{- end }}
{{- if .ValidatingWebhooks }}
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: {{ .ProjectName }}-validating-webhook-configuration
  namespace: {{ "{{ .Release.Namespace }}" }}
  annotations:
    {{` + "`" + `{{- if .Values.certmanager.enable }}` + "`" + `}}
    cert-manager.io/inject-ca-from: "{{` + "`" + `{{ $.Release.Namespace }}` + "`" + `}}/serving-cert"
    {{` + "`" + `{{- end }}` + "`" + `}}
  labels:
    {{ "{{- include \"chart.labels\" . | nindent 4 }}" }}
webhooks:
  {{- range .ValidatingWebhooks }}
  - name: {{ .Name }}
    clientConfig:
      service:
        name: {{ .ServiceName }}
        namespace: {{ "{{ .Release.Namespace }}" }}
        path: {{ .Path }}
    failurePolicy: {{ .FailurePolicy }}
    sideEffects: {{ .SideEffects }}
    admissionReviewVersions:
      {{- range .AdmissionReviewVersions }}
      - {{ . }}
      {{- end }}
    rules:
      {{- range .Rules }}
      - operations:
          {{- range .Operations }}
          - {{ . }}
          {{- end }}
        apiGroups:
          {{- range .APIGroups }}
          - {{ . }}
          {{- end }}
        apiVersions:
          {{- range .APIVersions }}
          - {{ . }}
          {{- end }}
        resources:
          {{- range .Resources }}
          - {{ . }}
          {{- end }}
      {{- end }}
  {{- end }}
{{- end }}
{{` + "`" + `{{- end }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/chart.go
================================================
/*
Copyright 2024 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 templates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmChart{}

// HelmChart scaffolds a file that defines the Helm chart structure
type HelmChart struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmChart) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "Chart.yaml")
	}

	f.TemplateBody = helmChartTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const helmChartTemplate = `apiVersion: v2
name: {{ .ProjectName }}
description: A Helm chart to distribute the project {{ .ProjectName }}
type: application
version: 0.1.0
appVersion: "0.1.0"
icon: "https://example.com/icon.png"
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/github/test_chart.go
================================================
/*
Copyright 2024 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 github

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmChartCI{}

// HelmChartCI scaffolds the GitHub Action for testing Helm charts
type HelmChartCI struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmChartCI) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join(".github", "workflows", "test-chart.yml")
	}

	f.TemplateBody = testChartTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

//nolint:lll
const testChartTemplate = `name: Test Chart

on:
  push:
  pull_request:

jobs:
  test-e2e:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install the latest version of kind
        run: |
          curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind

      - name: Verify kind installation
        run: kind version

      - name: Create kind cluster
        run: kind create cluster

      - name: Prepare {{ .ProjectName }}
        run: |
          go mod tidy
          make docker-build IMG={{ .ProjectName }}:v0.1.0
          kind load docker-image {{ .ProjectName }}:v0.1.0

      - name: Install Helm
        run: |
          curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

      - name: Verify Helm installation
        run: helm version

      - name: Lint Helm Chart
        run: |
          helm lint ./dist/chart

# TODO: Uncomment if cert-manager is enabled
#      - name: Install cert-manager via Helm
#        run: |
#          helm repo add jetstack https://charts.jetstack.io
#          helm repo update
#          helm install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace --set crds.enabled=true
#
#      - name: Wait for cert-manager to be ready
#        run: |
#          kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager
#          kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-cainjector
#          kubectl wait --namespace cert-manager --for=condition=available --timeout=300s deployment/cert-manager-webhook

# TODO: Uncomment if Prometheus is enabled
#      - name: Install Prometheus Operator CRDs
#        run: |
#          helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
#          helm repo update
#          helm install prometheus-crds prometheus-community/prometheus-operator-crds
#
#      - name: Install Prometheus via Helm
#        run: |
#          helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
#          helm repo update
#          helm install prometheus prometheus-community/prometheus --namespace monitoring --create-namespace
#
#      - name: Wait for Prometheus to be ready
#        run: |
#          kubectl wait --namespace monitoring --for=condition=available --timeout=300s deployment/prometheus-server

      - name: Install Helm chart for project
        run: |
          helm install my-release ./dist/chart --create-namespace --namespace {{ .ProjectName }}-system

      - name: Check Helm release status
        run: |
          helm status my-release --namespace {{ .ProjectName }}-system

# TODO: Uncomment if prometheus.enabled is set to true to confirm that the ServiceMonitor gets created
#      - name: Check Presence of ServiceMonitor
#        run: |
#          kubectl wait --namespace {{ .ProjectName }}-system --for=jsonpath='{.kind}'=ServiceMonitor servicemonitor/{{ .ProjectName }}-controller-manager-metrics-monitor
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/helmignore.go
================================================
/*
Copyright 2024 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 templates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmIgnore{}

// HelmIgnore scaffolds a file that defines the .helmignore for Helm packaging
type HelmIgnore struct {
	machinery.TemplateMixin
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmIgnore) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", ".helmignore")
	}

	f.TemplateBody = helmIgnoreTemplate

	f.IfExistsAction = machinery.SkipFile

	return nil
}

const helmIgnoreTemplate = `# Patterns to ignore when building Helm packages.
# Operating system files
.DS_Store

# Version control directories
.git/
.gitignore
.bzr/
.hg/
.hgignore
.svn/

# Backup and temporary files
*.swp
*.tmp
*.bak
*.orig
*~

# IDE and editor-related files
.idea/
.vscode/

# Helm chart artifacts
dist/chart/*.tgz
`


================================================
FILE: pkg/plugins/optional/helm/v1alpha/scaffolds/internal/templates/values.go
================================================
/*
Copyright 2024 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 templates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmValues{}

// HelmValues scaffolds a file that defines the values.yaml structure for the Helm chart
type HelmValues struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// DeployImages stores the images used for the DeployImage plugin
	DeployImages map[string]string
	// Force if true allows overwriting the scaffolded file
	Force bool
	// HasWebhooks is true when webhooks were found in the config
	HasWebhooks bool
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmValues) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join("dist", "chart", "values.yaml")
	}
	f.TemplateBody = helmValuesTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

const helmValuesTemplate = `# [MANAGER]: Manager Deployment Configurations
controllerManager:
  replicas: 1
  container:
    image:
      repository: controller
      tag: latest
    imagePullPolicy: IfNotPresent
    args:
      - "--leader-elect"
      - "--metrics-bind-address=:8443"
      - "--health-probe-bind-address=:8081"
    resources:
      limits:
        cpu: 500m
        memory: 128Mi
      requests:
        cpu: 10m
        memory: 64Mi
    livenessProbe:
      initialDelaySeconds: 15
      periodSeconds: 20
      httpGet:
        path: /healthz
        port: 8081
    readinessProbe:
      initialDelaySeconds: 5
      periodSeconds: 10
      httpGet:
        path: /readyz
        port: 8081
    {{- if .DeployImages }}
    env:
    {{- range $kind, $image := .DeployImages }}
      {{ $kind }}_IMAGE: {{ $image }}
    {{- end }}
    {{- end }}
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
          - "ALL"
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  terminationGracePeriodSeconds: 10
  serviceAccountName: {{ .ProjectName }}-controller-manager

# [RBAC]: To enable RBAC (Permissions) configurations
rbac:
  enable: true

# [CRDs]: To enable the CRDs
crd:
  # This option determines whether the CRDs are included
  # in the installation process.
  enable: true

  # Enabling this option adds the "helm.sh/resource-policy": keep
  # annotation to the CRD, ensuring it remains installed even when
  # the Helm release is uninstalled.
  # NOTE: Removing the CRDs will also remove all cert-manager CR(s)
  # (Certificates, Issuers, ...) due to garbage collection.
  keep: true

# [METRICS]: Set to true to generate manifests for exporting metrics.
# To disable metrics export set false, and ensure that the
# ControllerManager argument "--metrics-bind-address=:8443" is removed.
metrics:
  enable: true
{{ if .HasWebhooks }}
# [WEBHOOKS]: Webhooks configuration
# The following configuration is automatically generated from the manifests
# generated by controller-gen. To update run 'make manifests' and
# the edit command with the '--force' flag
webhook:
  enable: true
{{ end }}
# [PROMETHEUS]: To enable a ServiceMonitor to export metrics to Prometheus set true
prometheus:
  enable: false

# [CERT-MANAGER]: To enable cert-manager injection to webhooks set true
certmanager:
  enable: {{ .HasWebhooks }}

# [NETWORK POLICIES]: To enable NetworkPolicies set true
networkPolicy:
  enable: false
`


================================================
FILE: pkg/plugins/optional/helm/v2alpha/edit.go
================================================
/*
Copyright 2025 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 v2alpha

import (
	"errors"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/pflag"
	"go.yaml.in/yaml/v3"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds"
)

const (
	// DefaultManifestsFile is the default path for kustomize output manifests
	DefaultManifestsFile = "dist/install.yaml"
	// DefaultOutputDir is the default output directory for Helm charts
	DefaultOutputDir = "dist"
	// v1AlphaPluginKey is the deprecated v1-alpha plugin key
	v1AlphaPluginKey = "helm.kubebuilder.io/v1-alpha"
)

var _ plugin.EditSubcommand = &editSubcommand{}

type editSubcommand struct {
	config        config.Config
	force         bool
	manifestsFile string
	outputDir     string
}

//nolint:lll
func (p *editSubcommand) UpdateMetadata(cliMeta plugin.CLIMetadata, subcmdMeta *plugin.SubcommandMetadata) {
	subcmdMeta.Description = `Generate a Helm chart from your project's kustomize output.

Parses 'make build-installer' output (dist/install.yaml) and generates chart to allow easy
distribution of your project. When enabled, adds Helm helpers targets to Makefile`

	subcmdMeta.Examples = fmt.Sprintf(`# Generate Helm chart from default manifests (dist/install.yaml) to default output (dist/)
  %[1]s edit --plugins=%[2]s

# Generate Helm chart and overwrite existing files (useful for updates)
  %[1]s edit --plugins=%[2]s --force

# Generate Helm chart from a custom manifests file
  %[1]s edit --plugins=%[2]s --manifests=path/to/custom-install.yaml

# Generate Helm chart to a custom output directory
  %[1]s edit --plugins=%[2]s --output-dir=charts

# Generate from custom manifests to custom output directory
  %[1]s edit --plugins=%[2]s --manifests=manifests/install.yaml --output-dir=helm-charts

# Typical workflow:
  make build-installer  # Generate dist/install.yaml with latest changes
  %[1]s edit --plugins=%[2]s  # Generate/update Helm chart in dist/chart/

**NOTE**: Chart.yaml is never overwritten (contains user-managed version info).
Without --force, the plugin also preserves values.yaml, NOTES.txt, _helpers.tpl, .helmignore,
and .github/workflows/test-chart.yml.
All other template files in templates/ are always regenerated to match your current
kustomize output. Use --force to regenerate all files except Chart.yaml.

The generated chart structure mirrors your config/ directory:
/chart/
├── Chart.yaml
├── values.yaml
├── .helmignore
└── templates/
    ├── NOTES.txt
    ├── _helpers.tpl
    ├── rbac/
    ├── manager/
    ├── webhook/
    └── ...
`, cliMeta.CommandName, plugin.KeyFor(Plugin{}))
}

func (p *editSubcommand) BindFlags(fs *pflag.FlagSet) {
	fs.BoolVar(&p.force, "force", false, "if true, regenerates all the files")
	fs.StringVar(&p.manifestsFile, "manifests", DefaultManifestsFile,
		"path to the YAML file containing Kubernetes manifests from kustomize output")
	fs.StringVar(&p.outputDir, "output-dir", DefaultOutputDir, "output directory for the generated Helm chart")
}

func (p *editSubcommand) InjectConfig(c config.Config) error {
	p.config = c
	return nil
}

func (p *editSubcommand) Scaffold(fs machinery.Filesystem) error {
	// If using default manifests file, ensure it exists by running make build-installer
	if p.manifestsFile == DefaultManifestsFile {
		if err := p.ensureManifestsExist(); err != nil {
			slog.Warn("Failed to generate default manifests file", "error", err, "file", p.manifestsFile)
		}
	}

	scaffolder := scaffolds.NewKustomizeHelmScaffolder(p.config, p.force, p.manifestsFile, p.outputDir)
	scaffolder.InjectFS(fs)
	err := scaffolder.Scaffold()
	if err != nil {
		return fmt.Errorf("error scaffolding Helm chart: %w", err)
	}

	// Remove deprecated v1-alpha plugin entry from PROJECT file
	// This must happen in Scaffold (before config is saved) to be persisted
	p.removeV1AlphaPluginEntry()

	// Save plugin config to PROJECT file
	key := plugin.GetPluginKeyForConfig(p.config.GetPluginChain(), Plugin{})
	canonicalKey := plugin.KeyFor(Plugin{})
	cfg := pluginConfig{}
	isFirstRun := false
	if err = p.config.DecodePluginConfig(key, &cfg); err != nil {
		switch {
		case errors.As(err, &config.UnsupportedFieldError{}):
			// Config version doesn't support plugin metadata
			return nil
		case errors.As(err, &config.PluginKeyNotFoundError{}):
			// This is the first time the plugin is run
			isFirstRun = true
			if key != canonicalKey {
				if err2 := p.config.DecodePluginConfig(canonicalKey, &cfg); err2 != nil {
					if errors.As(err2, &config.UnsupportedFieldError{}) {
						return nil
					}
					if !errors.As(err2, &config.PluginKeyNotFoundError{}) {
						return fmt.Errorf("error decoding plugin configuration: %w", err2)
					}
				} else {
					// Found config under canonical key, not first run
					isFirstRun = false
				}
			}
		default:
			return fmt.Errorf("error decoding plugin configuration: %w", err)
		}
	}

	// Update configuration with current parameters
	cfg.ManifestsFile = p.manifestsFile
	cfg.OutputDir = p.outputDir

	if err = p.config.EncodePluginConfig(key, cfg); err != nil {
		return fmt.Errorf("error encoding plugin configuration: %w", err)
	}

	// Add Helm deployment targets to Makefile only on first run
	if isFirstRun {
		slog.Info("adding Helm deployment targets to Makefile...")
		// Extract namespace from manifests for accurate Makefile generation
		namespace := p.extractNamespaceFromManifests()
		if err := p.addHelmMakefileTargets(namespace); err != nil {
			slog.Warn("failed to add Helm targets to Makefile", "error", err)
		}
	}

	return nil
}

// ensureManifestsExist runs make build-installer to generate the default manifests file
func (p *editSubcommand) ensureManifestsExist() error {
	slog.Info("Generating default manifests file", "file", p.manifestsFile)

	// Run the required make targets to generate the manifests file
	targets := []string{"manifests", "generate", "build-installer"}
	for _, target := range targets {
		if err := util.RunCmd(fmt.Sprintf("Running make %s", target), "make", target); err != nil {
			return fmt.Errorf("make %s failed: %w", target, err)
		}
	}

	// Verify the file was created
	if _, err := os.Stat(p.manifestsFile); err != nil {
		return fmt.Errorf("manifests file %s was not created: %w", p.manifestsFile, err)
	}

	slog.Info("Successfully generated manifests file", "file", p.manifestsFile)
	return nil
}

// PostScaffold automatically uncomments cert-manager installation when webhooks are present
func (p *editSubcommand) PostScaffold() error {
	hasWebhooks := hasWebhooksWith(p.config)

	if hasWebhooks {
		workflowFile := filepath.Join(".github", "workflows", "test-chart.yml")
		if _, err := os.Stat(workflowFile); err != nil {
			slog.Info(
				"Workflow file not found, unable to uncomment cert-manager installation",
				"error", err,
				"file", workflowFile,
			)
			return nil
		}
		target := `
#      - name: Install cert-manager via Helm (wait for readiness)
#        run: |
#          helm repo add jetstack https://charts.jetstack.io
#          helm repo update
#          helm install cert-manager jetstack/cert-manager \
#            --namespace cert-manager \
#            --create-namespace \
#            --set crds.enabled=true \
#            --wait \
#            --timeout 300s`
		if err := util.UncommentCode(workflowFile, target, "#"); err != nil {
			hasUncommented, errCheck := util.HasFileContentWith(workflowFile, "- name: Install cert-manager via Helm")
			if !hasUncommented || errCheck != nil {
				slog.Warn("Failed to uncomment cert-manager installation in workflow file", "error", err, "file", workflowFile)
			}
		} else {
			target = `# TODO: Uncomment if cert-manager is enabled`
			_ = util.ReplaceInFile(workflowFile, target, "")
		}
	}
	return nil
}

// addHelmMakefileTargets appends Helm deployment targets to the Makefile if they don't already exist
func (p *editSubcommand) addHelmMakefileTargets(namespace string) error {
	makefilePath := "Makefile"
	if _, err := os.Stat(makefilePath); os.IsNotExist(err) {
		return fmt.Errorf("makefile not found")
	}

	// Get the Helm Makefile targets
	helmTargets := getHelmMakefileTargets(p.config.GetProjectName(), namespace, p.outputDir)

	// Append the targets if they don't already exist
	if err := util.AppendCodeIfNotExist(makefilePath, helmTargets); err != nil {
		return fmt.Errorf("failed to append Helm targets to Makefile: %w", err)
	}

	slog.Info("added Helm deployment targets to Makefile",
		"targets", "helm-deploy, helm-uninstall, helm-status, helm-history, helm-rollback")
	return nil
}

// extractNamespaceFromManifests parses the manifests file to extract the manager namespace.
// Returns projectName-system if manifests don't exist or namespace not found.
func (p *editSubcommand) extractNamespaceFromManifests() string {
	// Default to project-name-system pattern
	defaultNamespace := p.config.GetProjectName() + "-system"

	// If manifests file doesn't exist, use default
	if _, err := os.Stat(p.manifestsFile); os.IsNotExist(err) {
		return defaultNamespace
	}

	// Parse the manifests to get the namespace
	file, err := os.Open(p.manifestsFile)
	if err != nil {
		return defaultNamespace
	}
	defer func() {
		_ = file.Close()
	}()

	// Parse YAML documents looking for the manager Deployment
	decoder := yaml.NewDecoder(file)
	for {
		var doc map[string]any
		if err := decoder.Decode(&doc); err != nil {
			if err == io.EOF {
				break
			}
			continue
		}

		// Check if this is a Deployment (manager)
		if kind, ok := doc["kind"].(string); ok && kind == "Deployment" {
			if metadata, ok := doc["metadata"].(map[string]any); ok {
				// Check if it's the manager deployment
				if name, ok := metadata["name"].(string); ok && strings.Contains(name, "controller-manager") {
					// Extract namespace from the manager Deployment
					if namespace, ok := metadata["namespace"].(string); ok && namespace != "" {
						return namespace
					}
				}
			}
		}
	}

	// Fallback to default if manager Deployment not found
	return defaultNamespace
}

// getHelmMakefileTargets returns the Helm Makefile targets as a string
// following the same patterns as the existing Makefile deployment section
func getHelmMakefileTargets(projectName, namespace, outputDir string) string {
	if outputDir == "" {
		outputDir = "dist"
	}

	// Use the project name as default for release name
	release := projectName

	return helmMakefileTemplate(namespace, release, outputDir)
}

// helmMakefileTemplate returns the Helm deployment section template
// This follows the same pattern as the Kustomize deployment section in the Go plugin
const helmMakefileTemplateFormat = `
##@ Helm Deployment

## Helm binary to use for deploying the chart
HELM ?= helm
## Namespace to deploy the Helm release
HELM_NAMESPACE ?= %s
## Name of the Helm release
HELM_RELEASE ?= %s
## Path to the Helm chart directory
HELM_CHART_DIR ?= %s/chart
## Additional arguments to pass to helm commands
HELM_EXTRA_ARGS ?=

.PHONY: install-helm
install-helm: ## Install the latest version of Helm.
	@command -v $(HELM) >/dev/null 2>&1 || { \
		echo "Installing Helm..." && \
		curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
	}

.PHONY: helm-deploy
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
	$(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
		--namespace $(HELM_NAMESPACE) \
		--create-namespace \
		--set manager.image.repository=$${IMG%%:*} \
		--set manager.image.tag=$${IMG##*:} \
		--wait \
		--timeout 5m \
		$(HELM_EXTRA_ARGS)

.PHONY: helm-uninstall
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
	$(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-status
helm-status: ## Show Helm release status.
	$(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-history
helm-history: ## Show Helm release history.
	$(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-rollback
helm-rollback: ## Rollback to previous Helm release.
	$(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
`

func helmMakefileTemplate(namespace, release, outputDir string) string {
	return fmt.Sprintf(helmMakefileTemplateFormat, namespace, release, outputDir)
}

func hasWebhooksWith(c config.Config) bool {
	resources, err := c.GetResources()
	if err != nil {
		return false
	}

	for _, res := range resources {
		if res.HasDefaultingWebhook() || res.HasValidationWebhook() || res.HasConversionWebhook() {
			return true
		}
	}

	return false
}

// removeV1AlphaPluginEntry removes the deprecated helm.kubebuilder.io/v1-alpha plugin entry.
// This must be called from Scaffold (before config is saved) for changes to be persisted.
func (p *editSubcommand) removeV1AlphaPluginEntry() {
	// Only attempt to remove if using v3 config (which supports plugin configs)
	cfg, ok := p.config.(*cfgv3.Cfg)
	if !ok {
		return
	}

	// Check if v1-alpha plugin entry exists
	if cfg.Plugins == nil {
		return
	}

	if _, exists := cfg.Plugins[v1AlphaPluginKey]; exists {
		delete(cfg.Plugins, v1AlphaPluginKey)
		slog.Info("removed deprecated v1-alpha plugin entry")
	}
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/edit_test.go
================================================
/*
Copyright 2025 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 v2alpha

import (
	"os"
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"
	"github.com/spf13/pflag"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/config/store/yaml"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
)

var _ = Describe("editSubcommand", func() {
	var (
		editCmd *editSubcommand
		cfg     config.Config
		fs      machinery.Filesystem
	)

	BeforeEach(func() {
		// Create test config
		memFs := afero.NewMemMapFs()
		fs = machinery.Filesystem{FS: memFs}
		store := yaml.New(fs)

		// Create a basic PROJECT file
		projectContent := `domain: example.com
layout:
- go.kubebuilder.io/v4
projectName: test-project
repo: example.com/test-project
version: "3"
`
		err := afero.WriteFile(memFs, "PROJECT", []byte(projectContent), 0o644)
		Expect(err).NotTo(HaveOccurred())

		err = store.LoadFrom("PROJECT")
		Expect(err).NotTo(HaveOccurred())

		cfg = store.Config()

		// Create edit subcommand
		editCmd = &editSubcommand{
			config: cfg,
		}
	})

	Context("UpdateMetadata", func() {
		It("should set correct metadata", func() {
			cliMeta := plugin.CLIMetadata{CommandName: "kubebuilder"}
			meta := plugin.SubcommandMetadata{}
			editCmd.UpdateMetadata(cliMeta, &meta)

			Expect(meta.Description).To(ContainSubstring("Generate a Helm chart"))
			Expect(meta.Description).To(ContainSubstring("kustomize"))
			Expect(meta.Examples).NotTo(BeEmpty())
		})
	})

	Context("BindFlags", func() {
		It("should bind flags correctly", func() {
			flagSet := pflag.NewFlagSet("test", pflag.ContinueOnError)
			editCmd.BindFlags(flagSet)

			// Check that flags were added
			manifestsFlag := flagSet.Lookup("manifests")
			Expect(manifestsFlag).NotTo(BeNil())
			Expect(manifestsFlag.DefValue).To(Equal(DefaultManifestsFile))

			outputFlag := flagSet.Lookup("output-dir")
			Expect(outputFlag).NotTo(BeNil())
			Expect(outputFlag.DefValue).To(Equal(DefaultOutputDir))

			forceFlag := flagSet.Lookup("force")
			Expect(forceFlag).NotTo(BeNil())
		})
	})

	Context("InjectConfig", func() {
		It("should inject config correctly", func() {
			err := editCmd.InjectConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
			Expect(editCmd.config).To(Equal(cfg))
		})
	})

	Context("hasWebhooksWith", func() {
		It("should return false for config without webhooks", func() {
			result := hasWebhooksWith(cfg)
			Expect(result).To(BeFalse())
		})
	})

	Context("removeV1AlphaPluginEntry", func() {
		It("should remove v1-alpha plugin entry if it exists", func() {
			// Add v1-alpha plugin entry to config
			err := cfg.EncodePluginConfig(v1AlphaPluginKey, map[string]any{})
			Expect(err).NotTo(HaveOccurred())

			// Verify it exists
			var v1AlphaCfg map[string]any
			err = cfg.DecodePluginConfig(v1AlphaPluginKey, &v1AlphaCfg)
			Expect(err).NotTo(HaveOccurred())

			// Remove it
			editCmd.removeV1AlphaPluginEntry()

			// Verify it was removed from in-memory config
			err = cfg.DecodePluginConfig(v1AlphaPluginKey, &v1AlphaCfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("plugin key"))
		})

		It("should not error when v1-alpha entry does not exist", func() {
			// Should not panic or error
			editCmd.removeV1AlphaPluginEntry()
		})

		It("should not error when plugins map is nil", func() {
			// Create a fresh config without any plugins
			memFs := afero.NewMemMapFs()
			freshFs := machinery.Filesystem{FS: memFs}
			store := yaml.New(freshFs)

			projectContent := `domain: example.com
layout:
- go.kubebuilder.io/v4
projectName: test-project
repo: example.com/test-project
version: "3"
`
			err := afero.WriteFile(memFs, "PROJECT", []byte(projectContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = store.LoadFrom("PROJECT")
			Expect(err).NotTo(HaveOccurred())

			freshCfg := store.Config()
			freshEditCmd := &editSubcommand{config: freshCfg}

			// Should not panic or error
			freshEditCmd.removeV1AlphaPluginEntry()
		})

		It("should persist v1-alpha removal when config is saved", func() {
			// Create a store to test actual persistence
			memFs := afero.NewMemMapFs()
			testFs := machinery.Filesystem{FS: memFs}
			store := yaml.New(testFs)

			// Create PROJECT file with v1-alpha plugin entry
			projectContent := `domain: example.com
layout:
- go.kubebuilder.io/v4
plugins:
  helm.kubebuilder.io/v1-alpha: {}
projectName: test-project
repo: example.com/test-project
version: "3"
`
			err := afero.WriteFile(memFs, "PROJECT", []byte(projectContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = store.LoadFrom("PROJECT")
			Expect(err).NotTo(HaveOccurred())

			testCfg := store.Config()
			testEditCmd := &editSubcommand{config: testCfg}

			// Verify v1-alpha entry exists before removal
			var v1AlphaCfg map[string]any
			err = testCfg.DecodePluginConfig(v1AlphaPluginKey, &v1AlphaCfg)
			Expect(err).NotTo(HaveOccurred())

			// Remove v1-alpha entry
			testEditCmd.removeV1AlphaPluginEntry()

			// Save config to disk
			err = store.Save()
			Expect(err).NotTo(HaveOccurred())

			// Reload config from disk
			err = store.LoadFrom("PROJECT")
			Expect(err).NotTo(HaveOccurred())

			reloadedCfg := store.Config()

			// Verify v1-alpha entry is gone after reload
			err = reloadedCfg.DecodePluginConfig(v1AlphaPluginKey, &v1AlphaCfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("plugin key"))
		})
	})

	Context("PostScaffold", func() {
		BeforeEach(func() {
			// Create the directory structure
			err := fs.FS.MkdirAll(".github/workflows", 0o755)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should not modify workflow file when no webhooks present", func() {
			// Create test workflow file
			workflowContent := `name: Test Chart
on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

#      - name: Install cert-manager via Helm
#        run: |
#          helm repo add jetstack https://charts.jetstack.io
#          helm repo update
#          helm install cert-manager jetstack/cert-manager \
#            --namespace cert-manager --create-namespace --set crds.enabled=true
#
#      - name: Wait for cert-manager to be ready
#        run: |
#          kubectl wait --namespace cert-manager --for=condition=available \
#            --timeout=300s deployment/cert-manager
#          kubectl wait --namespace cert-manager --for=condition=available \
#            --timeout=300s deployment/cert-manager-cainjector
#          kubectl wait --namespace cert-manager --for=condition=available \
#            --timeout=300s deployment/cert-manager-webhook
`
			workflowPath := filepath.Join(".github", "workflows", "test-chart.yml")
			err := afero.WriteFile(fs.FS, workflowPath, []byte(workflowContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = editCmd.PostScaffold()
			Expect(err).NotTo(HaveOccurred())

			// Content should remain unchanged
			content, err := afero.ReadFile(fs.FS, workflowPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(content)).To(ContainSubstring("#      - name: Install cert-manager via Helm"))
		})

		It("should handle missing workflow file gracefully", func() {
			editCmd.config = cfg
			err := editCmd.PostScaffold()
			Expect(err).NotTo(HaveOccurred()) // Should not error even if file doesn't exist
		})
	})

	Context("addHelmMakefileTargets", func() {
		var tmpDir string

		BeforeEach(func() {
			var err error
			tmpDir, err = os.MkdirTemp("", "helm-makefile-test-*")
			Expect(err).NotTo(HaveOccurred())

			// Change to temp directory
			err = os.Chdir(tmpDir)
			Expect(err).NotTo(HaveOccurred())

			editCmd.outputDir = DefaultOutputDir
		})

		AfterEach(func() {
			// Clean up temp directory
			if tmpDir != "" {
				_ = os.RemoveAll(tmpDir)
			}
		})

		It("should add Helm targets to Makefile when it exists", func() {
			// Create a basic Makefile
			makefileContent := `IMG ?= controller:latest

##@ Development

.PHONY: build
build: ## Build manager binary.
	go build -o bin/manager cmd/main.go
`
			err := os.WriteFile("Makefile", []byte(makefileContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = editCmd.addHelmMakefileTargets("test-project-system")
			Expect(err).NotTo(HaveOccurred())

			// Verify Helm targets were added
			content, err := os.ReadFile("Makefile")
			Expect(err).NotTo(HaveOccurred())

			contentStr := string(content)
			Expect(contentStr).To(ContainSubstring("##@ Helm Deployment"))
			Expect(contentStr).To(ContainSubstring("## Helm binary to use for deploying the chart"))
			Expect(contentStr).To(ContainSubstring("HELM ?= helm"))
			Expect(contentStr).To(ContainSubstring("## Namespace to deploy the Helm release"))
			Expect(contentStr).To(ContainSubstring("HELM_NAMESPACE ?= test-project-system"))
			Expect(contentStr).To(ContainSubstring("## Name of the Helm release"))
			Expect(contentStr).To(ContainSubstring("HELM_RELEASE ?= test-project"))
			Expect(contentStr).To(ContainSubstring("## Path to the Helm chart directory"))
			Expect(contentStr).To(ContainSubstring("HELM_CHART_DIR ?= dist/chart"))
			Expect(contentStr).To(ContainSubstring("## Additional arguments to pass to helm commands"))
			Expect(contentStr).To(ContainSubstring("HELM_EXTRA_ARGS ?="))
			Expect(contentStr).To(ContainSubstring(".PHONY: install-helm"))
			Expect(contentStr).To(ContainSubstring("install-helm: ## Install the latest version of Helm."))
			Expect(contentStr).To(ContainSubstring(".PHONY: helm-deploy"))
			Expect(contentStr).To(ContainSubstring(
				"helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG."))
			Expect(contentStr).To(ContainSubstring("--set manager.image.repository=$${IMG%:*}"))
			Expect(contentStr).To(ContainSubstring("--set manager.image.tag=$${IMG##*:}"))
			Expect(contentStr).To(ContainSubstring(".PHONY: helm-uninstall"))
			Expect(contentStr).To(ContainSubstring(
				"helm-uninstall: ## Uninstall the Helm release from the K8s cluster."))
			Expect(contentStr).To(ContainSubstring(".PHONY: helm-status"))
			Expect(contentStr).To(ContainSubstring("helm-status: ## Show Helm release status."))
			Expect(contentStr).To(ContainSubstring(".PHONY: helm-history"))
			Expect(contentStr).To(ContainSubstring("helm-history: ## Show Helm release history."))
			Expect(contentStr).To(ContainSubstring(".PHONY: helm-rollback"))
			Expect(contentStr).To(ContainSubstring("helm-rollback: ## Rollback to previous Helm release."))
		})

		It("should not duplicate Helm targets if already present", func() {
			// Create a Makefile that already has Helm targets (exact match to template)
			makefileContent := `IMG ?= controller:latest

.PHONY: build
build: ## Build manager binary.
	go build -o bin/manager cmd/main.go

##@ Helm Deployment

## Helm binary to use for deploying the chart
HELM ?= helm
## Namespace to deploy the Helm release
HELM_NAMESPACE ?= test-project-system
## Name of the Helm release
HELM_RELEASE ?= test-project
## Path to the Helm chart directory
HELM_CHART_DIR ?= dist/chart
## Additional arguments to pass to helm commands
HELM_EXTRA_ARGS ?=

.PHONY: install-helm
install-helm: ## Install the latest version of Helm.
	@command -v $(HELM) >/dev/null 2>&1 || { \
		echo "Installing Helm..." && \
		curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
	}

.PHONY: helm-deploy
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
	$(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
		--namespace $(HELM_NAMESPACE) \
		--create-namespace \
		--set manager.image.repository=$${IMG%:*} \
		--set manager.image.tag=$${IMG##*:} \
		--wait \
		--timeout 5m \
		$(HELM_EXTRA_ARGS)

.PHONY: helm-uninstall
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
	$(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-status
helm-status: ## Show Helm release status.
	$(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-history
helm-history: ## Show Helm release history.
	$(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-rollback
helm-rollback: ## Rollback to previous Helm release.
	$(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
`
			err := os.WriteFile("Makefile", []byte(makefileContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			err = editCmd.addHelmMakefileTargets("test-project-system")
			Expect(err).NotTo(HaveOccurred())

			// Verify targets were not duplicated
			content, err := os.ReadFile("Makefile")
			Expect(err).NotTo(HaveOccurred())

			// Count occurrences of helm-deploy target
			contentStr := string(content)
			helmDeployCount := 0
			for i := 0; i < len(contentStr)-len("helm-deploy:"); i++ {
				if contentStr[i:i+len("helm-deploy:")] == "helm-deploy:" {
					helmDeployCount++
				}
			}
			Expect(helmDeployCount).To(Equal(1)) // Should only appear once
		})

		It("should return error when Makefile does not exist", func() {
			err := editCmd.addHelmMakefileTargets("test-project-system")
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring("makefile not found"))
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/makefile_test.go
================================================
/*
Copyright 2025 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 v2alpha

import (
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
)

var _ = Describe("Helm Makefile Targets", func() {
	var (
		tmpDir       string
		makefilePath string
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "helm-makefile-test-*")
		Expect(err).NotTo(HaveOccurred())
		makefilePath = filepath.Join(tmpDir, "Makefile")

		// Create a basic Makefile with some existing content
		initialContent := `# Basic Makefile
.PHONY: all
all: build

.PHONY: build
build:
	go build -o bin/manager cmd/main.go

##@ Deployment
.PHONY: deploy
deploy:
	kubectl apply -k config/default
`
		err = os.WriteFile(makefilePath, []byte(initialContent), 0o644)
		Expect(err).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	It("should add Helm deployment targets without duplication", func() {
		// Get the Helm targets
		helmTargets := getHelmMakefileTargets("test-project", "test-system", "dist")

		By("appending Helm targets for the first time")
		err := util.AppendCodeIfNotExist(makefilePath, helmTargets)
		Expect(err).NotTo(HaveOccurred())

		// Read the Makefile
		content, err := os.ReadFile(makefilePath)
		Expect(err).NotTo(HaveOccurred())
		makefileContent := string(content)

		By("verifying Helm section was added")
		Expect(makefileContent).To(ContainSubstring("##@ Helm Deployment"))
		Expect(makefileContent).To(ContainSubstring("HELM ?= helm"))
		Expect(makefileContent).To(ContainSubstring(".PHONY: install-helm"))
		Expect(makefileContent).To(ContainSubstring("install-helm: ## Install the latest version of Helm."))
		Expect(makefileContent).To(ContainSubstring(".PHONY: helm-deploy"))
		Expect(makefileContent).To(ContainSubstring("helm-deploy: install-helm ##"))
		Expect(makefileContent).To(ContainSubstring(".PHONY: helm-uninstall"))

		// Count occurrences
		helmSectionCount := strings.Count(makefileContent, "##@ Helm Deployment")
		helmDeployCount := strings.Count(makefileContent, ".PHONY: helm-deploy")
		helmInstallCount := strings.Count(makefileContent, "install-helm: ## Install the latest version of Helm.")
		Expect(helmSectionCount).To(Equal(1), "Helm section should appear exactly once")
		Expect(helmDeployCount).To(Equal(1), "helm-deploy should appear exactly once")
		Expect(helmInstallCount).To(Equal(1), "install-helm target should appear exactly once")

		By("appending Helm targets again (should not duplicate)")
		err = util.AppendCodeIfNotExist(makefilePath, helmTargets)
		Expect(err).NotTo(HaveOccurred())

		// Read the Makefile again
		content2, err := os.ReadFile(makefilePath)
		Expect(err).NotTo(HaveOccurred())
		makefileContent2 := string(content2)

		By("verifying no duplication occurred")
		helmSectionCount2 := strings.Count(makefileContent2, "##@ Helm Deployment")
		helmDeployCount2 := strings.Count(makefileContent2, ".PHONY: helm-deploy")
		helmInstallCount2 := strings.Count(makefileContent2, "install-helm: ## Install the latest version of Helm.")
		Expect(helmSectionCount2).To(Equal(1), "Helm section should still appear exactly once")
		Expect(helmDeployCount2).To(Equal(1), "helm-deploy should still appear exactly once")
		Expect(helmInstallCount2).To(Equal(1), "install-helm target should still appear exactly once")

		// Verify content is identical (no duplication)
		Expect(makefileContent2).To(Equal(makefileContent), "Makefile should be unchanged after second append")
	})

	It("should generate correct Helm targets template", func() {
		helmTargets := getHelmMakefileTargets("my-project", "my-project-system", "dist")

		By("verifying template contains expected sections")
		Expect(helmTargets).To(ContainSubstring("##@ Helm Deployment"))
		Expect(helmTargets).To(ContainSubstring("HELM_NAMESPACE ?= my-project-system"))
		Expect(helmTargets).To(ContainSubstring("HELM_RELEASE ?= my-project"))
		Expect(helmTargets).To(ContainSubstring("HELM_CHART_DIR ?= dist/chart"))
		Expect(helmTargets).To(ContainSubstring("install-helm: ## Install the latest version of Helm."))
		Expect(helmTargets).To(ContainSubstring("helm-deploy: install-helm ##"))
		Expect(helmTargets).To(ContainSubstring("helm-uninstall:"))
		Expect(helmTargets).To(ContainSubstring("helm-status:"))
		Expect(helmTargets).To(ContainSubstring("helm-history:"))
		Expect(helmTargets).To(ContainSubstring("helm-rollback:"))
	})

	It("should handle custom output directory", func() {
		helmTargets := getHelmMakefileTargets("test-project", "test-system", "custom-charts")

		By("verifying custom directory is used")
		Expect(helmTargets).To(ContainSubstring("HELM_CHART_DIR ?= custom-charts/chart"))
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/plugin.go
================================================
/*
Copyright 2025 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 v2alpha

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/model/stage"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
)

const pluginName = "helm." + plugins.DefaultNameQualifier

var (
	pluginVersion            = plugin.Version{Number: 2, Stage: stage.Alpha}
	supportedProjectVersions = []config.Version{cfgv3.Version}
)

// Plugin implements the plugin.Full interface
type Plugin struct {
	editSubcommand
}

var _ plugin.Edit = Plugin{}

// PluginConfig defines the structure that will be used to track the data
type pluginConfig struct {
	ManifestsFile string `json:"manifests,omitempty"`
	OutputDir     string `json:"output,omitempty"`
}

// Name returns the name of the plugin
func (Plugin) Name() string { return pluginName }

// Version returns the version of the Helm plugin
func (Plugin) Version() plugin.Version { return pluginVersion }

// SupportedProjectVersions returns an array with all project versions supported by the plugin
func (Plugin) SupportedProjectVersions() []config.Version { return supportedProjectVersions }

// GetEditSubcommand will return the subcommand which is responsible for adding and/or edit a helm chart
func (p Plugin) GetEditSubcommand() plugin.EditSubcommand { return &p.editSubcommand }

// Description returns a short description of the plugin
func (Plugin) Description() string {
	return "Generates a Helm chart for project distribution"
}

// DeprecationWarning define the deprecation message or return empty when plugin is not deprecated
func (p Plugin) DeprecationWarning() string {
	return ""
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/plugin_test.go
================================================
/*
Copyright 2025 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 v2alpha

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
)

var _ = Describe("Plugin", func() {
	var p Plugin

	BeforeEach(func() {
		p = Plugin{}
	})

	Context("Name", func() {
		It("should return the correct plugin name", func() {
			Expect(p.Name()).To(Equal("helm.kubebuilder.io"))
		})
	})

	Context("Version", func() {
		It("should return version 2-alpha", func() {
			version := p.Version()
			Expect(version.Number).To(Equal(2))
			Expect(version.Stage.String()).To(Equal("alpha"))
		})
	})

	Context("SupportedProjectVersions", func() {
		It("should support project version 3", func() {
			versions := p.SupportedProjectVersions()
			expectedVersion := config.Version{Number: 3}
			Expect(versions).To(ContainElement(expectedVersion))
		})
	})

	Context("GetEditSubcommand", func() {
		It("should return an edit subcommand", func() {
			subcommand := p.GetEditSubcommand()
			Expect(subcommand).NotTo(BeNil())
		})
	})

	Context("DeprecationWarning", func() {
		It("should return empty string since v2-alpha is not deprecated", func() {
			warning := p.DeprecationWarning()
			Expect(warning).To(BeEmpty())
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/chart_generation_integration_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"
	helmChartLoader "helm.sh/helm/v3/pkg/chart/loader"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("Chart Generation Integration Tests", func() {
	var (
		fs             machinery.Filesystem
		tmpDir         string
		manifestsFile  string
		outputDir      string
		projectConfig  config.Config
		scaffolderBase *editKustomizeScaffolder
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "helm-chart-gen-test-*")
		Expect(err).NotTo(HaveOccurred())

		err = os.Chdir(tmpDir)
		Expect(err).NotTo(HaveOccurred())

		fs = machinery.Filesystem{
			FS: afero.NewBasePathFs(afero.NewOsFs(), tmpDir),
		}

		projectConfig = cfgv3.New()
		projectConfig.SetProjectName("test-project")
		projectConfig.SetDomain("example.io")

		manifestsFile = filepath.Join(tmpDir, "dist", "install.yaml")
		outputDir = "dist"
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	Context("Basic Functionality", func() {
		It("should generate valid helm chart with dynamic templates", func() {
			kustomizeYAML := createKustomizeWithCRDAndRBAC("test-project")
			err := setupKustomizeFile(manifestsFile, kustomizeYAML)
			Expect(err).NotTo(HaveOccurred())

			scaffolderBase = &editKustomizeScaffolder{
				config:        projectConfig,
				fs:            fs,
				manifestsFile: manifestsFile,
				outputDir:     outputDir,
			}

			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			chartPath := filepath.Join(tmpDir, outputDir, "chart")

			By("verifying templates directory structure matches config/ structure")
			expectedDirs := []string{
				"templates/manager",
				"templates/rbac",
				"templates/crd",
			}
			for _, dir := range expectedDirs {
				dirPath := filepath.Join(chartPath, dir)
				info, err := os.Stat(dirPath)
				Expect(err).NotTo(HaveOccurred(), "Directory %s should exist", dir)
				Expect(info.IsDir()).To(BeTrue())
			}

			By("verifying manager deployment template exists")
			managerTemplate := filepath.Join(chartPath, "templates", "manager", "manager.yaml")
			_, err = os.Stat(managerTemplate)
			Expect(err).NotTo(HaveOccurred())

			By("verifying CRD templates exist")
			crdDir := filepath.Join(chartPath, "templates", "crd")
			files, err := afero.ReadDir(afero.NewOsFs(), crdDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(files).ToNot(BeEmpty())

			By("verifying Chart.yaml exists and is valid")
			chart, err := helmChartLoader.LoadDir(chartPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(chart.Validate()).To(Succeed())
			Expect(chart.Name()).To(Equal("test-project"))

			By("verifying essential files exist")
			essentialFiles := []string{
				"Chart.yaml",
				"values.yaml",
				".helmignore",
				"templates/_helpers.tpl",
			}
			for _, file := range essentialFiles {
				filePath := filepath.Join(chartPath, file)
				_, err := os.Stat(filePath)
				Expect(err).NotTo(HaveOccurred(), "File %s should exist", file)
			}
		})
	})

	Context("Webhook and Cert-Manager Integration", func() {
		It("should generate webhook templates with cert-manager integration and proper templating", func() {
			kustomizeYAML := createKustomizeWithWebhooksAndCertManager("e2e-test")
			err := setupKustomizeFile(manifestsFile, kustomizeYAML)
			Expect(err).NotTo(HaveOccurred())

			projectConfig.SetProjectName("e2e-test")
			scaffolderBase = &editKustomizeScaffolder{
				config:        projectConfig,
				fs:            fs,
				manifestsFile: manifestsFile,
				outputDir:     outputDir,
			}

			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			chartPath := filepath.Join(tmpDir, outputDir, "chart")

			By("verifying webhook directory exists")
			webhookDir := filepath.Join(chartPath, "templates", "webhook")
			info, err := os.Stat(webhookDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(info.IsDir()).To(BeTrue())

			By("verifying webhook configuration files exist")
			files, err := afero.ReadDir(afero.NewOsFs(), webhookDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(files).ToNot(BeEmpty())

			By("verifying webhook files contain webhook configurations")
			foundValidatingWebhook := false
			for _, file := range files {
				if file.IsDir() {
					continue
				}
				webhookFile := filepath.Join(webhookDir, file.Name())
				content, err := afero.ReadFile(afero.NewOsFs(), webhookFile)
				Expect(err).NotTo(HaveOccurred())
				contentStr := string(content)
				if strings.Contains(contentStr, "ValidatingWebhookConfiguration") {
					foundValidatingWebhook = true
					break
				}
			}
			Expect(foundValidatingWebhook).To(BeTrue(), "Expected to find ValidatingWebhookConfiguration in webhook templates")

			By("verifying cert-manager templates exist")
			certManagerDir := filepath.Join(chartPath, "templates", "cert-manager")
			certInfo, err := os.Stat(certManagerDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(certInfo.IsDir()).To(BeTrue())

			By("verifying cert-manager is enabled in values.yaml")
			valuesPath := filepath.Join(chartPath, "values.yaml")
			valuesContent, err := afero.ReadFile(afero.NewOsFs(), valuesPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(valuesContent)).To(ContainSubstring("certManager:"))
			Expect(string(valuesContent)).To(ContainSubstring("enable: true"))
		})
	})

	Context("Chart Name Handling", func() {
		It("should use project name in helpers regardless of kustomize namePrefix", func() {
			// Kustomize output with custom namePrefix
			kustomizeYAML := createKustomizeWithCustomPrefix("custom-prefix", "test-project")
			err := setupKustomizeFile(manifestsFile, kustomizeYAML)
			Expect(err).NotTo(HaveOccurred())

			projectConfig.SetProjectName("test-project")
			scaffolderBase = &editKustomizeScaffolder{
				config:        projectConfig,
				fs:            fs,
				manifestsFile: manifestsFile,
				outputDir:     outputDir,
			}

			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			chartPath := filepath.Join(tmpDir, outputDir, "chart")

			By("verifying _helpers.tpl uses project name, not kustomize prefix")
			helpersContent, err := os.ReadFile(filepath.Join(chartPath, "templates", "_helpers.tpl"))
			Expect(err).NotTo(HaveOccurred())
			helpersStr := string(helpersContent)

			// Should contain project name-based templates
			Expect(helpersStr).To(ContainSubstring(`define "test-project.name"`))
			Expect(helpersStr).To(ContainSubstring(`define "test-project.fullname"`))
			Expect(helpersStr).To(ContainSubstring(`define "test-project.resourceName"`))
			Expect(helpersStr).To(ContainSubstring(`define "test-project.namespaceName"`))

			// Should NOT contain kustomize prefix in template definitions
			Expect(helpersStr).NotTo(ContainSubstring(`define "custom-prefix.name"`))
			Expect(helpersStr).NotTo(ContainSubstring(`define "custom-prefix.fullname"`))

			By("verifying templates use project name helpers, not kustomize prefix")
			managerContent, err := os.ReadFile(filepath.Join(chartPath, "templates", "manager", "manager.yaml"))
			Expect(err).NotTo(HaveOccurred())
			managerStr := string(managerContent)

			Expect(managerStr).To(ContainSubstring(`include "test-project`))
			Expect(managerStr).NotTo(ContainSubstring(`custom-prefix-controller-manager`),
				"Manager template should not contain hardcoded kustomize prefix")
		})

		It("should properly template cert-manager resources when chart name is used", func() {
			kustomizeYAML := createKustomizeWithWebhooksAndCertManager("e2e-test")
			err := setupKustomizeFile(manifestsFile, kustomizeYAML)
			Expect(err).NotTo(HaveOccurred())

			projectConfig.SetProjectName("e2e-test")
			scaffolderBase = &editKustomizeScaffolder{
				config:        projectConfig,
				fs:            fs,
				manifestsFile: manifestsFile,
				outputDir:     outputDir,
			}

			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			chartPath := filepath.Join(tmpDir, outputDir, "chart")
			chartName := "e2e-test"

			By("validating issuer name uses chartname.resourceName for 63-char safety")
			issuerPath := filepath.Join(chartPath, "templates", "cert-manager", "selfsigned-issuer.yaml")
			content, err := afero.ReadFile(afero.NewOsFs(), issuerPath)
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)

			expected := `name: {{ include "` + chartName + `.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }}`
			Expect(contentStr).To(ContainSubstring(expected),
				"Issuer name should use "+chartName+".resourceName template")
			Expect(contentStr).NotTo(ContainSubstring("e2e-test-selfsigned-issuer"),
				"Issuer name should not be hardcoded to project name")

			By("validating certificate issuerRef uses chartname.resourceName")
			certManagerDir := filepath.Join(chartPath, "templates", "cert-manager")
			files, err := afero.ReadDir(afero.NewOsFs(), certManagerDir)
			Expect(err).NotTo(HaveOccurred())

			foundCertificate := false
			for _, file := range files {
				if file.IsDir() || !strings.HasSuffix(file.Name(), ".yaml") || file.Name() == "selfsigned-issuer.yaml" {
					continue
				}

				certPath := filepath.Join(certManagerDir, file.Name())
				content, err := afero.ReadFile(afero.NewOsFs(), certPath)
				Expect(err).NotTo(HaveOccurred())
				contentStr := string(content)

				if strings.Contains(contentStr, "kind: Certificate") {
					foundCertificate = true
					expected := `name: {{ include "` + chartName + `.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }}`
					Expect(contentStr).To(ContainSubstring(expected),
						"Certificate issuerRef should use "+chartName+".resourceName template in file "+file.Name())
				}
			}
			Expect(foundCertificate).To(BeTrue(), "Expected to find at least one Certificate resource")

			By("validating cert-manager annotations use chartname.resourceName")
			// Check webhook configurations
			webhookDir := filepath.Join(chartPath, "templates", "webhook")
			if exists, _ := afero.DirExists(afero.NewOsFs(), webhookDir); exists {
				files, err := afero.ReadDir(afero.NewOsFs(), webhookDir)
				Expect(err).NotTo(HaveOccurred())

				for _, file := range files {
					if file.IsDir() || !strings.HasSuffix(file.Name(), ".yaml") {
						continue
					}

					webhookPath := filepath.Join(webhookDir, file.Name())
					content, err := afero.ReadFile(afero.NewOsFs(), webhookPath)
					Expect(err).NotTo(HaveOccurred())
					contentStr := string(content)

					if strings.Contains(contentStr, "cert-manager.io/inject-ca-from") {
						expected := `{{ include "` + chartName + `.resourceName" (dict "suffix" "serving-cert" "context" $) }}`
						Expect(contentStr).To(ContainSubstring(expected),
							"cert-manager.io/inject-ca-from annotation should use "+chartName+".resourceName in "+file.Name())
						Expect(contentStr).NotTo(ContainSubstring("e2e-test-serving-cert"),
							"cert-manager.io/inject-ca-from annotation should not be hardcoded in "+file.Name())
					}
				}
			}

			By("validating app.kubernetes.io/name label uses chartname.name template")
			// Check all cert-manager resources
			certManagerFiles, err := afero.ReadDir(afero.NewOsFs(), certManagerDir)
			Expect(err).NotTo(HaveOccurred())

			for _, file := range certManagerFiles {
				if file.IsDir() || !strings.HasSuffix(file.Name(), ".yaml") {
					continue
				}

				filePath := filepath.Join(certManagerDir, file.Name())
				content, err := afero.ReadFile(afero.NewOsFs(), filePath)
				Expect(err).NotTo(HaveOccurred())
				contentStr := string(content)

				if strings.Contains(contentStr, "app.kubernetes.io/name:") {
					Expect(contentStr).To(ContainSubstring(`app.kubernetes.io/name: {{ include "`+chartName+`.name" . }}`),
						"app.kubernetes.io/name label should use "+chartName+".name template in "+file.Name())
					Expect(contentStr).NotTo(ContainSubstring("app.kubernetes.io/name: e2e-test"),
						"app.kubernetes.io/name label should not be hardcoded in "+file.Name())
				}
			}
		})
	})

	Context("Custom Output Directory", func() {
		It("should support custom output directory via --output-dir flag", func() {
			kustomizeYAML := createBasicKustomizeOutput("test-project")
			err := setupKustomizeFile(manifestsFile, kustomizeYAML)
			Expect(err).NotTo(HaveOccurred())

			customOutputDir := "custom-charts"
			scaffolderBase = &editKustomizeScaffolder{
				config:        projectConfig,
				fs:            fs,
				manifestsFile: manifestsFile,
				outputDir:     customOutputDir,
			}

			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			chartPath := filepath.Join(tmpDir, customOutputDir, "chart")

			By("verifying chart exists in custom directory")
			info, err := os.Stat(chartPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(info.IsDir()).To(BeTrue())

			By("verifying Chart.yaml in custom directory")
			chartFile := filepath.Join(chartPath, "Chart.yaml")
			_, err = os.Stat(chartFile)
			Expect(err).NotTo(HaveOccurred())
		})
	})

	Context("Values Extraction", func() {
		It("should extract deployment configuration to values.yaml", func() {
			kustomizeYAML := createKustomizeWithFullDeploymentConfig("test-project")
			err := setupKustomizeFile(manifestsFile, kustomizeYAML)
			Expect(err).NotTo(HaveOccurred())

			scaffolderBase = &editKustomizeScaffolder{
				config:        projectConfig,
				fs:            fs,
				manifestsFile: manifestsFile,
				outputDir:     outputDir,
			}

			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			chartPath := filepath.Join(tmpDir, outputDir, "chart")
			valuesPath := filepath.Join(chartPath, "values.yaml")
			valuesContent, err := os.ReadFile(valuesPath)
			Expect(err).NotTo(HaveOccurred())
			valuesStr := string(valuesContent)

			By("verifying image configuration is extracted")
			Expect(valuesStr).To(ContainSubstring("image:"))
			Expect(valuesStr).To(ContainSubstring("repository:"))
			Expect(valuesStr).To(ContainSubstring("tag:"))
			Expect(valuesStr).To(ContainSubstring("pullPolicy:"))

			By("verifying resources are extracted")
			Expect(valuesStr).To(ContainSubstring("resources:"))
			Expect(valuesStr).To(ContainSubstring("limits:"))
			Expect(valuesStr).To(ContainSubstring("requests:"))

			By("verifying security context is extracted")
			Expect(valuesStr).To(ContainSubstring("securityContext:"))
		})
	})
})

// Helper functions to create kustomize YAML outputs for different scenarios

func createBasicKustomizeOutput(projectName string) string {
	return `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
  name: ` + projectName + `-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
  name: ` + projectName + `-controller-manager
  namespace: ` + projectName + `-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
    control-plane: controller-manager
  name: ` + projectName + `-controller-manager
  namespace: ` + projectName + `-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - name: manager
        image: controller:latest
`
}

func createKustomizeWithCRDAndRBAC(projectName string) string {
	return createBasicKustomizeOutput(projectName) + `---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: cronjobs.batch.tutorial.kubebuilder.io
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
spec:
  group: batch.tutorial.kubebuilder.io
  names:
    kind: CronJob
    listKind: CronJobList
    plural: cronjobs
    singular: cronjob
  scope: Namespaced
  versions:
  - name: v1
    served: true
    storage: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: ` + projectName + `-manager-role
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
`
}

func createKustomizeWithWebhooks(projectName string) string {
	return createBasicKustomizeOutput(projectName) + `---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
  name: ` + projectName + `-webhook-service
  namespace: ` + projectName + `-system
spec:
  ports:
  - port: 443
    targetPort: 9443
  selector:
    control-plane: controller-manager
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: ` + projectName + `-validating-webhook-configuration
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: ` + projectName + `-webhook-service
      namespace: ` + projectName + `-system
      path: /validate
  name: validate.example.com
  sideEffects: None
`
}

func createKustomizeWithWebhooksAndCertManager(projectName string) string {
	return createKustomizeWithWebhooks(projectName) + `---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
  name: ` + projectName + `-selfsigned-issuer
  namespace: ` + projectName + `-system
spec:
  selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
  name: ` + projectName + `-serving-cert
  namespace: ` + projectName + `-system
spec:
  dnsNames:
  - ` + projectName + `-webhook-service.` + projectName + `-system.svc
  - ` + projectName + `-webhook-service.` + projectName + `-system.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: ` + projectName + `-selfsigned-issuer
  secretName: webhook-server-cert
`
}

func createKustomizeWithCustomPrefix(prefix, projectName string) string {
	return `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
  name: ` + prefix + `-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
    control-plane: controller-manager
  name: ` + prefix + `-controller-manager
  namespace: ` + prefix + `-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - name: manager
        image: controller:latest
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: ` + projectName + `
  name: ` + prefix + `-controller-manager-metrics-service
  namespace: ` + prefix + `-system
spec:
  ports:
  - port: 8443
    targetPort: 8443
`
}

func createKustomizeWithFullDeploymentConfig(projectName string) string {
	return `---
apiVersion: v1
kind: Namespace
metadata:
  name: ` + projectName + `-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ` + projectName + `-controller-manager
  namespace: ` + projectName + `-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - name: manager
        image: myrepo/controller:v1.2.3
        imagePullPolicy: IfNotPresent
        args:
        - --leader-elect
        - --metrics-bind-address=:8443
        - --health-probe-bind-address=:8081
        ports:
        - containerPort: 9443
          name: webhook-server
          protocol: TCP
        env:
        - name: TEST_ENV
          value: "test-value"
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
`
}

func setupKustomizeFile(filePath, content string) error {
	if err := os.MkdirAll(filepath.Dir(filePath), 0o755); err != nil {
		return err
	}
	return os.WriteFile(filePath, []byte(content), 0o644)
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/chart_never_overwrite_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"os"
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("Chart.yaml Never Overwrite Test", func() {
	var (
		fs            machinery.Filesystem
		tmpDir        string
		manifestsFile string
		outputDir     string
		projectConfig config.Config
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "helm-chart-never-overwrite-*")
		Expect(err).NotTo(HaveOccurred())

		err = os.Chdir(tmpDir)
		Expect(err).NotTo(HaveOccurred())

		fs = machinery.Filesystem{
			FS: afero.NewBasePathFs(afero.NewOsFs(), tmpDir),
		}

		projectConfig = cfgv3.New()
		projectConfig.SetProjectName("test-project")
		projectConfig.SetDomain("example.io")

		manifestsFile = filepath.Join(tmpDir, "dist", "install.yaml")
		outputDir = "dist"

		// Create minimal kustomize output
		kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  name: test-project-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: manager
        image: controller:latest
`

		err = os.MkdirAll(filepath.Dir(manifestsFile), 0o755)
		Expect(err).NotTo(HaveOccurred())
		err = os.WriteFile(manifestsFile, []byte(kustomizeYAML), 0o644)
		Expect(err).NotTo(HaveOccurred())
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	It("should NEVER overwrite Chart.yaml even with --force=true", func() {
		scaffolder := &editKustomizeScaffolder{
			config:        projectConfig,
			fs:            fs,
			force:         false,
			manifestsFile: manifestsFile,
			outputDir:     outputDir,
		}

		// First scaffold
		err := scaffolder.Scaffold()
		Expect(err).NotTo(HaveOccurred())

		chartPath := filepath.Join(tmpDir, outputDir, "chart", "Chart.yaml")

		// Read initial Chart.yaml
		initialContent, err := os.ReadFile(chartPath)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(initialContent)).To(ContainSubstring("name: test-project"))
		Expect(string(initialContent)).To(ContainSubstring("version: 0.1.0"))

		// Customize Chart.yaml with user version
		customChartYAML := `apiVersion: v2
name: test-project
description: My custom description
type: application
version: 1.2.3
appVersion: "1.2.3"
icon: "https://mycompany.com/icon.png"
maintainers:
  - name: John Doe
    email: john@example.com
`
		err = os.WriteFile(chartPath, []byte(customChartYAML), 0o644)
		Expect(err).NotTo(HaveOccurred())

		// Scaffold again WITHOUT force
		err = scaffolder.Scaffold()
		Expect(err).NotTo(HaveOccurred())

		// Verify Chart.yaml was NOT overwritten
		content, err := os.ReadFile(chartPath)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(Equal(customChartYAML), "Chart.yaml should not be overwritten without --force")
		Expect(string(content)).To(ContainSubstring("version: 1.2.3"))
		Expect(string(content)).To(ContainSubstring("John Doe"))

		// Scaffold again WITH force=true
		scaffolder.force = true
		err = scaffolder.Scaffold()
		Expect(err).NotTo(HaveOccurred())

		// Verify Chart.yaml STILL was NOT overwritten (never overwritten even with force)
		content, err = os.ReadFile(chartPath)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(Equal(customChartYAML), "Chart.yaml should NEVER be overwritten, even with --force")
		Expect(string(content)).To(ContainSubstring("version: 1.2.3"))
		Expect(string(content)).To(ContainSubstring("John Doe"))
		Expect(string(content)).To(ContainSubstring("My custom description"))
	})

	It("should preserve Chart.yaml on initial scaffold if file already exists", func() {
		// Create Chart.yaml before any scaffolding
		chartPath := filepath.Join(tmpDir, outputDir, "chart", "Chart.yaml")
		err := os.MkdirAll(filepath.Dir(chartPath), 0o755)
		Expect(err).NotTo(HaveOccurred())

		preexistingChart := `apiVersion: v2
name: my-existing-chart
version: 99.99.99
`
		err = os.WriteFile(chartPath, []byte(preexistingChart), 0o644)
		Expect(err).NotTo(HaveOccurred())

		// First scaffold with force=true
		scaffolder := &editKustomizeScaffolder{
			config:        projectConfig,
			fs:            fs,
			force:         true,
			manifestsFile: manifestsFile,
			outputDir:     outputDir,
		}

		err = scaffolder.Scaffold()
		Expect(err).NotTo(HaveOccurred())

		// Verify Chart.yaml was preserved even on first scaffold with force
		content, err := os.ReadFile(chartPath)
		Expect(err).NotTo(HaveOccurred())
		Expect(string(content)).To(Equal(preexistingChart), "Chart.yaml should be preserved even on first scaffold with --force")
		Expect(string(content)).To(ContainSubstring("my-existing-chart"))
		Expect(string(content)).To(ContainSubstring("99.99.99"))
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/edit_kustomize.go
================================================
/*
Copyright 2025 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 scaffolds

import (
	"fmt"
	"log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"strings"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates"
	charttemplates "sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart-templates"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/github"
)

const (
	defaultManifestsFile = "dist/install.yaml"
)

var _ plugins.Scaffolder = &editKustomizeScaffolder{}

type editKustomizeScaffolder struct {
	config        config.Config
	fs            machinery.Filesystem
	force         bool
	manifestsFile string
	outputDir     string
}

// NewKustomizeHelmScaffolder returns a new Scaffolder for HelmPlugin using kustomize output
func NewKustomizeHelmScaffolder(cfg config.Config, force bool, manifestsFile, outputDir string) plugins.Scaffolder {
	return &editKustomizeScaffolder{
		config:        cfg,
		force:         force,
		manifestsFile: manifestsFile,
		outputDir:     outputDir,
	}
}

// InjectFS implements cmdutil.Scaffolder
func (s *editKustomizeScaffolder) InjectFS(fs machinery.Filesystem) {
	s.fs = fs
}

// Scaffold generates the complete Helm chart from kustomize output
func (s *editKustomizeScaffolder) Scaffold() error {
	slog.Info("Generating Helm Chart from kustomize output")

	// Ensure chart directory structure exists
	if err := s.ensureChartDirectoryExists(); err != nil {
		return fmt.Errorf("failed to create chart directory: %w", err)
	}

	// Generate fresh kustomize output if using default file
	if s.manifestsFile == defaultManifestsFile {
		if err := s.generateKustomizeOutput(); err != nil {
			return fmt.Errorf("failed to generate kustomize output: %w", err)
		}
	}

	// Parse the kustomize output into organized resource groups
	parser := kustomize.NewParser(s.manifestsFile)
	resources, err := parser.Parse()
	if err != nil {
		return fmt.Errorf("failed to parse kustomize output from %s: %w", s.manifestsFile, err)
	}

	// Warn if Custom Resource instances were found and will be ignored
	if len(resources.CustomResources) > 0 {
		slog.Warn(
			"Custom Resource instances found. They will be ignored and not included in the Helm chart",
			"count", len(resources.CustomResources),
			"note", "CRs are environment-specific and should be created manually after chart installation",
		)
		for _, cr := range resources.CustomResources {
			slog.Warn(
				"Ignoring Custom Resource instance",
				"kind", cr.GetKind(),
				"apiVersion", cr.GetAPIVersion(),
				"name", cr.GetName(),
			)
		}
	}

	// Analyze resources to determine chart features
	hasWebhooks := len(resources.WebhookConfigurations) > 0 || len(resources.Certificates) > 0
	// Prometheus is enabled when ServiceMonitor resources exist (../prometheus enabled)
	hasPrometheus := len(resources.ServiceMonitors) > 0
	// Metrics are enabled either when ServiceMonitor exists or when a metrics service is present
	hasMetrics := hasPrometheus
	if !hasMetrics {
		for _, svc := range resources.Services {
			if strings.Contains(svc.GetName(), "metrics") {
				hasMetrics = true
				break
			}
		}
	}

	// When Prometheus is enabled via kustomize, ensure any previously-generated
	// generic ServiceMonitor file is removed to avoid duplicates in the chart.
	if hasPrometheus {
		staleSM := filepath.Join(s.outputDir, "chart", "templates", "monitoring", "servicemonitor.yaml")
		if rmErr := s.fs.FS.Remove(staleSM); rmErr != nil && !os.IsNotExist(rmErr) {
			// Not fatal; log and continue
			slog.Warn("failed to remove stale generic ServiceMonitor", "path", staleSM, "error", rmErr)
		}
	}
	namePrefix := resources.EstimatePrefix(s.config.GetProjectName())
	chartName := s.config.GetProjectName()
	chartConverter := kustomize.NewChartConverter(resources, namePrefix, chartName, s.outputDir)
	deploymentConfig := chartConverter.ExtractDeploymentConfig()

	// Create scaffold for standard Helm chart files (uses machinery defaults 0755/0644).
	scaffold := machinery.NewScaffold(s.fs, machinery.WithConfig(s.config))

	// Define the standard Helm chart files to generate
	chartFiles := []machinery.Builder{
		&github.HelmChartCI{Force: s.force},
		&templates.HelmChart{OutputDir: s.outputDir, Force: s.force},
		&templates.HelmValuesBasic{
			// values.yaml with dynamic config
			HasWebhooks:      hasWebhooks,
			HasMetrics:       hasMetrics,
			DeploymentConfig: deploymentConfig,
			OutputDir:        s.outputDir,
			Force:            s.force,
		},
		&templates.HelmIgnore{OutputDir: s.outputDir, Force: s.force},
		&charttemplates.HelmHelpers{OutputDir: s.outputDir, Force: s.force},
		&charttemplates.Notes{
			OutputDir: s.outputDir,
			Force:     s.force,
		},
	}

	// Only scaffold the generic ServiceMonitor when the project does NOT already
	// provide one via kustomize (../prometheus). This avoids duplicate objects
	// with the same name within the Helm chart.
	if !hasPrometheus {
		// Find the metrics service name from parsed resources
		metricsServiceName := namePrefix + "-controller-manager-metrics-service"
		for _, svc := range resources.Services {
			if strings.Contains(svc.GetName(), "metrics-service") {
				metricsServiceName = svc.GetName()
				break
			}
		}

		chartFiles = append(chartFiles, &charttemplates.ServiceMonitor{
			OutputDir:   s.outputDir,
			ServiceName: metricsServiceName,
			Force:       s.force,
		})
	}

	// Generate template files from kustomize output
	if writeErr := chartConverter.WriteChartFiles(s.fs); writeErr != nil {
		return fmt.Errorf("failed to write chart template files: %w", writeErr)
	}

	// Generate standard Helm chart files
	if err = scaffold.Execute(chartFiles...); err != nil {
		return fmt.Errorf("failed to generate Helm chart files: %w", err)
	}

	slog.Info("Helm Chart generation completed successfully")
	return nil
}

// generateKustomizeOutput runs make build-installer to generate the manifests file
func (s *editKustomizeScaffolder) generateKustomizeOutput() error {
	slog.Info("Generating kustomize output with make build-installer")

	// Check if Makefile exists
	if _, err := os.Stat("Makefile"); os.IsNotExist(err) {
		return fmt.Errorf("makefile not found in current directory")
	}

	// Run make build-installer
	cmd := exec.Command("make", "build-installer")
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to run make build-installer: %w", err)
	}

	// Verify that the manifests file was created
	if _, err := os.Stat(defaultManifestsFile); os.IsNotExist(err) {
		return fmt.Errorf("%s was not generated by make build-installer", defaultManifestsFile)
	}

	return nil
}

// ensureChartDirectoryExists creates the chart directory structure if it doesn't exist
func (s *editKustomizeScaffolder) ensureChartDirectoryExists() error {
	dirs := []string{
		filepath.Join(s.outputDir, "chart"),
		filepath.Join(s.outputDir, "chart", "templates"),
	}

	for _, dir := range dirs {
		if err := os.MkdirAll(dir, 0o755); err != nil {
			return fmt.Errorf("failed to create directory %s: %w", dir, err)
		}
	}

	return nil
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/extras_integration_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
	"sigs.k8s.io/kubebuilder/v4/pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize"
)

var _ = Describe("Extras Directory Integration Test", func() {
	var (
		fs     machinery.Filesystem
		tmpDir string
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "helm-extras-test-*")
		Expect(err).NotTo(HaveOccurred())

		// Change to tmpDir so relative paths work correctly
		err = os.Chdir(tmpDir)
		Expect(err).NotTo(HaveOccurred())

		fs = machinery.Filesystem{
			FS: afero.NewBasePathFs(afero.NewOsFs(), tmpDir),
		}
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	Context("when converting Kustomize output with extra resources", func() {
		It("should place ConfigMap in extras directory with proper labels", func() {
			// Create a simulated kustomize output with standard resources and a ConfigMap
			kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
    control-plane: controller-manager
  name: test-project-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
    control-plane: controller-manager
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - name: manager
        image: controller:latest
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: custom-config
  namespace: test-project-system
data:
  key1: value1
  key2: value2
---
apiVersion: v1
kind: Secret
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: custom-secret
  namespace: test-project-system
type: Opaque
data:
  password: c2VjcmV0Cg==
`

			By("writing kustomize output to a file")
			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			By("parsing the kustomize output")
			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())
			Expect(resources).NotTo(BeNil())

			By("verifying ConfigMap and Secret are in Other category")
			Expect(resources.Other).To(HaveLen(2))

			By("converting to Helm chart")
			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying extras directory was created")
			extrasDir := filepath.Join("dist", "chart", "templates", "extras")
			exists, err := afero.Exists(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "extras directory should exist")

			By("verifying extras directory contains the ConfigMap and Secret")
			files, err := afero.ReadDir(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(2), "extras should contain ConfigMap and Secret")

			var configMapFile, secretFile string
			for _, f := range files {
				if f.Name() == "custom-config.yaml" {
					configMapFile = f.Name()
				}
				if f.Name() == "custom-secret.yaml" {
					secretFile = f.Name()
				}
			}
			Expect(configMapFile).NotTo(BeEmpty(), "ConfigMap file should exist")
			Expect(secretFile).NotTo(BeEmpty(), "Secret file should exist")

			By("verifying ConfigMap has proper Helm templating")
			configMapPath := filepath.Join(extrasDir, configMapFile)
			content, err := afero.ReadFile(fs.FS, configMapPath)
			Expect(err).NotTo(HaveOccurred())
			configMapContent := string(content)

			// Verify namespace templating
			Expect(configMapContent).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"ConfigMap should have templated namespace")

			// Note: Resource names without the project prefix are kept as-is
			// This allows users to have custom resource names that don't follow the project naming convention
			Expect(configMapContent).To(ContainSubstring("name: custom-config"),
				"ConfigMap name should be preserved as-is when it doesn't match project prefix")

			// Verify standard Helm labels
			Expect(configMapContent).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`),
				"ConfigMap should have app.kubernetes.io/name label")
			Expect(configMapContent).To(ContainSubstring("app.kubernetes.io/instance: {{ .Release.Name }}"),
				"ConfigMap should have app.kubernetes.io/instance label")
			Expect(configMapContent).To(ContainSubstring("app.kubernetes.io/managed-by: {{ .Release.Service }}"),
				"ConfigMap should have app.kubernetes.io/managed-by label")
			Expect(configMapContent).To(ContainSubstring(`helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}`),
				"ConfigMap should have helm.sh/chart label")

			// Verify data is preserved
			Expect(configMapContent).To(ContainSubstring("key1: value1"),
				"ConfigMap data should be preserved")
			Expect(configMapContent).To(ContainSubstring("key2: value2"),
				"ConfigMap data should be preserved")

			By("verifying Secret has proper Helm templating")
			secretPath := filepath.Join(extrasDir, secretFile)
			content, err = afero.ReadFile(fs.FS, secretPath)
			Expect(err).NotTo(HaveOccurred())
			secretContent := string(content)

			// Verify namespace templating
			Expect(secretContent).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"Secret should have templated namespace")

			// Note: Resource names without the project prefix are kept as-is
			Expect(secretContent).To(ContainSubstring("name: custom-secret"),
				"Secret name should be preserved as-is when it doesn't match project prefix")

			// Verify standard Helm labels
			Expect(secretContent).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`),
				"Secret should have app.kubernetes.io/name label")
			Expect(secretContent).To(ContainSubstring("app.kubernetes.io/managed-by: {{ .Release.Service }}"),
				"Secret should have app.kubernetes.io/managed-by label")

			// Verify data is preserved
			Expect(secretContent).To(ContainSubstring("password: c2VjcmV0Cg=="),
				"Secret data should be preserved")
		})

		It("should place custom Service in extras directory", func() {
			kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: test-project-system
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: custom-service
  namespace: test-project-system
spec:
  ports:
  - port: 8080
    targetPort: 8080
  selector:
    app: custom
`

			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying custom Service is in extras directory")
			extrasDir := filepath.Join("dist", "chart", "templates", "extras")
			exists, err := afero.Exists(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())

			files, err := afero.ReadDir(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(1))
			Expect(files[0].Name()).To(Equal("custom-service.yaml"))

			By("verifying Service has proper Helm templating")
			servicePath := filepath.Join(extrasDir, files[0].Name())
			content, err := afero.ReadFile(fs.FS, servicePath)
			Expect(err).NotTo(HaveOccurred())
			serviceContent := string(content)

			Expect(serviceContent).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))
			Expect(serviceContent).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`))
		})

		It("should not place webhook or metrics services in extras", func() {
			kustomizeYAML := `---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: test-project-webhook-service
  namespace: test-project-system
spec:
  ports:
  - port: 443
    targetPort: 9443
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: test-project-controller-manager-metrics-service
  namespace: test-project-system
spec:
  ports:
  - port: 8443
    targetPort: 8443
`

			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying extras directory was NOT created")
			extrasDir := filepath.Join("dist", "chart", "templates", "extras")
			exists, err := afero.Exists(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeFalse(), "extras directory should not exist for webhook/metrics services")

			By("verifying webhook directory was created")
			webhookDir := filepath.Join("dist", "chart", "templates", "webhook")
			exists, err = afero.Exists(fs.FS, webhookDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())

			By("verifying metrics directory was created")
			metricsDir := filepath.Join("dist", "chart", "templates", "metrics")
			exists, err = afero.Exists(fs.FS, metricsDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())
		})
	})

	Context("RBAC resource placement", func() {
		It("should ensure all RBAC resources go to rbac directory, never to extras", func() {
			// Critical test: RBAC resources must NEVER end up in extras directory
			kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  name: test-project-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-project-manager-role
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-leader-election-role
  namespace: test-project-system
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-project-manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-leader-election-rolebinding
  namespace: test-project-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-leader-election-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-config
  namespace: test-project-system
data:
  key: value
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - name: manager
        image: controller:latest
`

			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			// Verify parser correctly categorized RBAC resources
			Expect(resources.ServiceAccount).NotTo(BeNil(), "ServiceAccount should be parsed")
			Expect(resources.ClusterRoles).To(HaveLen(1), "should have 1 ClusterRole")
			Expect(resources.Roles).To(HaveLen(1), "should have 1 Role")
			Expect(resources.ClusterRoleBindings).To(HaveLen(1), "should have 1 ClusterRoleBinding")
			Expect(resources.RoleBindings).To(HaveLen(1), "should have 1 RoleBinding")
			Expect(resources.Other).To(HaveLen(1), "ConfigMap should be in Other")

			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying rbac directory exists and contains all RBAC resources")
			rbacDir := filepath.Join("dist", "chart", "templates", "rbac")
			exists, err := afero.Exists(fs.FS, rbacDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "rbac directory must exist")

			rbacFiles, err := afero.ReadDir(fs.FS, rbacDir)
			Expect(err).NotTo(HaveOccurred())
			// Should have: 1 ServiceAccount + 1 ClusterRole + 1 Role + 1 ClusterRoleBinding + 1 RoleBinding = 5 files
			Expect(rbacFiles).To(HaveLen(5), "rbac directory should have exactly 5 RBAC files")

			By("verifying NO RBAC resources in extras directory")
			extrasDir := filepath.Join("dist", "chart", "templates", "extras")
			exists, err = afero.Exists(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "extras directory should exist for ConfigMap")

			extrasFiles, err := afero.ReadDir(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(extrasFiles).To(HaveLen(1), "extras should only have ConfigMap, not RBAC")

			// Verify the extras file is the ConfigMap, not any RBAC resource
			configMapFound := false
			for _, f := range extrasFiles {
				if strings.Contains(f.Name(), "custom-config") {
					configMapFound = true
				}
				// Ensure no RBAC-related files
				Expect(f.Name()).NotTo(ContainSubstring("role"), "no Role files in extras")
				Expect(f.Name()).NotTo(ContainSubstring("rolebinding"), "no RoleBinding files in extras")
				Expect(f.Name()).NotTo(ContainSubstring("serviceaccount"), "no ServiceAccount files in extras")
			}
			Expect(configMapFound).To(BeTrue(), "ConfigMap should be in extras")
		})
	})

	Context("when converting namespace-scoped RBAC resources", func() {
		It("should convert namespace-scoped Roles with explicit namespaces to Helm templates", func() {
			// This test validates the scenario from issue where namespace-scoped Roles
			// (used for cross-namespace permissions, leader election, etc.) must be
			// included in the generated Helm chart, not just the ClusterRole.
			kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
    control-plane: controller-manager
  name: test-project-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-project-manager-role
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
rules:
- apiGroups: ["example.com"]
  resources: ["myresources"]
  verbs: ["get", "list", "watch", "create", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-manager-role
  namespace: infrastructure
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "patch", "update", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-leader-election-role
  namespace: test-project-system
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-events-role
  namespace: production
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
rules:
- apiGroups: [""]
  resources: ["events"]
  verbs: ["create", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-project-manager-rolebinding
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-manager-rolebinding
  namespace: infrastructure
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-leader-election-rolebinding
  namespace: test-project-system
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-leader-election-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-events-rolebinding
  namespace: production
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-events-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
    control-plane: controller-manager
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - name: manager
        image: controller:latest
`

			By("writing kustomize output to a file")
			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			By("parsing the kustomize output")
			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())
			Expect(resources).NotTo(BeNil())

			By("verifying parser correctly categorized all RBAC resources")
			Expect(resources.ClusterRoles).To(HaveLen(1), "should have 1 ClusterRole")
			Expect(resources.Roles).To(HaveLen(3), "should have 3 namespace-scoped Roles")
			Expect(resources.ClusterRoleBindings).To(HaveLen(1), "should have 1 ClusterRoleBinding")
			Expect(resources.RoleBindings).To(HaveLen(3), "should have 3 RoleBindings")

			By("converting to Helm chart")
			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying rbac directory was created")
			rbacDir := filepath.Join("dist", "chart", "templates", "rbac")
			exists, err := afero.Exists(fs.FS, rbacDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "rbac directory should exist")

			By("verifying all RBAC files are present")
			rbacFiles, err := afero.ReadDir(fs.FS, rbacDir)
			Expect(err).NotTo(HaveOccurred())
			// Should have: 1 ServiceAccount + 1 ClusterRole + 1 ClusterRoleBinding + 3 Roles + 3 RoleBindings = 9 files
			Expect(rbacFiles).To(HaveLen(9), "should have 9 RBAC files total")

			By("verifying ClusterRole file exists")
			// ClusterRole has no namespace, so filename is just the name (with project prefix removed)
			clusterRolePath := filepath.Join(rbacDir, "manager-role.yaml")
			exists, err = afero.Exists(fs.FS, clusterRolePath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "ClusterRole file should exist")

			By("verifying infrastructure Role file exists")
			// Role has namespace, so filename includes namespace suffix: name-namespace.yaml
			infrastructureRolePath := filepath.Join(rbacDir, "manager-role-infrastructure.yaml")
			exists, err = afero.Exists(fs.FS, infrastructureRolePath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "infrastructure Role file should exist")

			By("verifying project namespace Role file exists")
			// Role in project namespace (test-project-system) should NOT have namespace suffix
			projectRolePath := filepath.Join(rbacDir, "leader-election-role.yaml")
			exists, err = afero.Exists(fs.FS, projectRolePath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "project namespace Role file should exist without suffix")

			By("verifying production Role file exists")
			// Role in cross-namespace should have namespace suffix
			productionRolePath := filepath.Join(rbacDir, "events-role-production.yaml")
			exists, err = afero.Exists(fs.FS, productionRolePath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "production Role file should exist with suffix")

			By("verifying infrastructure RoleBinding file exists")
			infrastructureBindingPath := filepath.Join(rbacDir, "manager-rolebinding-infrastructure.yaml")
			exists, err = afero.Exists(fs.FS, infrastructureBindingPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "infrastructure RoleBinding file should exist")

			By("verifying project namespace RoleBinding file exists")
			// RoleBinding in project namespace should NOT have namespace suffix
			projectBindingPath := filepath.Join(rbacDir, "leader-election-rolebinding.yaml")
			exists, err = afero.Exists(fs.FS, projectBindingPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "project namespace RoleBinding file should exist without suffix")

			By("verifying production RoleBinding file exists")
			// RoleBinding in cross-namespace should have namespace suffix
			productionBindingPath := filepath.Join(rbacDir, "events-rolebinding-production.yaml")
			exists, err = afero.Exists(fs.FS, productionBindingPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "production RoleBinding file should exist with suffix")

			By("verifying infrastructure Role has proper Helm templating")
			roleContent, err := afero.ReadFile(fs.FS, infrastructureRolePath)
			Expect(err).NotTo(HaveOccurred())
			roleContentStr := string(roleContent)

			// Verify namespace is preserved (not templated to .Release.Namespace)
			// because it's an explicit cross-namespace permission
			Expect(roleContentStr).To(ContainSubstring("namespace: infrastructure"),
				"Role should preserve explicit namespace for cross-namespace permissions")

			// Verify standard Helm labels
			Expect(roleContentStr).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`),
				"Role should have templated app.kubernetes.io/name label")

			// Verify name is templated
			Expect(roleContentStr).To(ContainSubstring(`name: {{ include "test-project.resourceName"`),
				"Role name should be templated")

			// Verify rules are preserved
			Expect(roleContentStr).To(ContainSubstring("apiGroups:"),
				"Role rules should be preserved")
			Expect(roleContentStr).To(ContainSubstring("- apps"),
				"Role should have apps API group")
			Expect(roleContentStr).To(ContainSubstring("- deployments"),
				"Role should have deployments resource")

			By("verifying project namespace Role has proper Helm templating")
			projRoleContent, err := afero.ReadFile(fs.FS, projectRolePath)
			Expect(err).NotTo(HaveOccurred())
			projRoleContentStr := string(projRoleContent)

			// Verify namespace is templated to .Release.Namespace for project namespace
			Expect(projRoleContentStr).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"Role in project namespace should template namespace to .Release.Namespace")
			Expect(projRoleContentStr).NotTo(ContainSubstring("namespace: test-project-system"),
				"Role should not have hardcoded project namespace")

			// Verify leader election permissions
			Expect(projRoleContentStr).To(ContainSubstring("- coordination.k8s.io"))
			Expect(projRoleContentStr).To(ContainSubstring("- leases"))

			By("verifying production Role has proper Helm templating")
			prodRoleContent, err := afero.ReadFile(fs.FS, productionRolePath)
			Expect(err).NotTo(HaveOccurred())
			prodRoleContentStr := string(prodRoleContent)

			// Verify namespace is preserved for cross-namespace Role
			Expect(prodRoleContentStr).To(ContainSubstring("namespace: production"),
				"Role should preserve explicit namespace for cross-namespace permissions")

			// Verify events permissions
			Expect(prodRoleContentStr).To(ContainSubstring("- events"))

			By("verifying infrastructure RoleBinding has proper Helm templating")
			bindingContent, err := afero.ReadFile(fs.FS, infrastructureBindingPath)
			Expect(err).NotTo(HaveOccurred())
			bindingContentStr := string(bindingContent)

			// Verify namespace is preserved
			Expect(bindingContentStr).To(ContainSubstring("namespace: infrastructure"),
				"RoleBinding should preserve explicit namespace")

			// Verify roleRef is templated
			Expect(bindingContentStr).To(ContainSubstring(`name: {{ include "test-project.resourceName"`),
				"RoleBinding roleRef should be templated")

			// Verify subjects namespace is templated (references the controller namespace)
			Expect(bindingContentStr).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"RoleBinding subject namespace should reference the release namespace")

			By("verifying project namespace RoleBinding has proper Helm templating")
			projBindingContent, err := afero.ReadFile(fs.FS, projectBindingPath)
			Expect(err).NotTo(HaveOccurred())
			projBindingContentStr := string(projBindingContent)

			// Verify namespace is templated for project namespace RoleBinding
			Expect(projBindingContentStr).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"RoleBinding metadata namespace should be templated for project namespace")

			// Verify standard Helm labels
			Expect(projBindingContentStr).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`),
				"RoleBinding should have templated labels")

			By("verifying production RoleBinding has proper Helm templating")
			prodBindingContent, err := afero.ReadFile(fs.FS, productionBindingPath)
			Expect(err).NotTo(HaveOccurred())
			prodBindingContentStr := string(prodBindingContent)

			// Verify namespace is preserved for cross-namespace RoleBinding
			Expect(prodBindingContentStr).To(ContainSubstring("namespace: production"),
				"RoleBinding should preserve explicit namespace for cross-namespace binding")

			// Subject namespace should still be templated (references the controller namespace)
			Expect(prodBindingContentStr).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"RoleBinding subject namespace should be templated to Release.Namespace")

			By("verifying ClusterRole does not have namespace field")
			clusterRoleContent, err := afero.ReadFile(fs.FS, clusterRolePath)
			Expect(err).NotTo(HaveOccurred())
			clusterRoleContentStr := string(clusterRoleContent)

			// ClusterRole should not have namespace field
			Expect(clusterRoleContentStr).NotTo(ContainSubstring("namespace:"),
				"ClusterRole should not have namespace field")
		})

		It("should preserve ANY namespace field that differs from manager namespace", func() {
			// This test validates that ANY namespace reference (metadata, subjects, etc.)
			// that is NOT the manager namespace gets preserved exactly as-is
			kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  name: test-project-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: external-sa
  namespace: external-namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-cross-ns-binding
  namespace: infrastructure
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: some-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
- kind: ServiceAccount
  name: external-sa
  namespace: external-namespace
- kind: ServiceAccount
  name: another-sa
  namespace: production
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      serviceAccountName: test-project-controller-manager
      containers:
      - name: manager
        image: controller:latest
`

			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying RoleBinding preserves all non-manager namespaces")
			rbacDir := filepath.Join("dist", "chart", "templates", "rbac")
			bindingPath := filepath.Join(rbacDir, "cross-ns-binding-infrastructure.yaml")
			exists, err := afero.Exists(fs.FS, bindingPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "RoleBinding file should exist")

			bindingContent, err := afero.ReadFile(fs.FS, bindingPath)
			Expect(err).NotTo(HaveOccurred())
			bindingStr := string(bindingContent)

			// Metadata namespace should be preserved (cross-namespace)
			Expect(bindingStr).To(ContainSubstring("namespace: infrastructure"),
				"metadata namespace should be preserved")

			// Manager namespace in subjects should be templated
			Expect(bindingStr).To(MatchRegexp(`kind: ServiceAccount\s+name:.*\s+namespace: \{\{ \.Release\.Namespace \}\}`),
				"manager namespace in subject should be templated")

			// External namespaces in subjects should be preserved
			Expect(bindingStr).To(ContainSubstring("namespace: external-namespace"),
				"external-namespace in subject should be preserved")
			Expect(bindingStr).To(ContainSubstring("namespace: production"),
				"production namespace in subject should be preserved")

			// Count namespace occurrences
			infrastructureCount := strings.Count(bindingStr, "namespace: infrastructure")
			externalCount := strings.Count(bindingStr, "namespace: external-namespace")
			productionCount := strings.Count(bindingStr, "namespace: production")
			releaseNsCount := strings.Count(bindingStr, "namespace: {{ .Release.Namespace }}")

			Expect(infrastructureCount).To(Equal(1), "should have 1 infrastructure namespace (metadata)")
			Expect(externalCount).To(Equal(1), "should have 1 external-namespace (subject)")
			Expect(productionCount).To(Equal(1), "should have 1 production namespace (subject)")
			Expect(releaseNsCount).To(BeNumerically(">=", 1), "should have at least 1 templated namespace (manager subject)")

			By("verifying external ServiceAccount preserves its namespace")
			saFiles, err := afero.ReadDir(fs.FS, rbacDir)
			Expect(err).NotTo(HaveOccurred())

			var externalSAFound bool
			for _, f := range saFiles {
				if strings.Contains(f.Name(), "external-sa") {
					externalSAFound = true
					saPath := filepath.Join(rbacDir, f.Name())
					saContent, err := afero.ReadFile(fs.FS, saPath)
					Expect(err).NotTo(HaveOccurred())
					saStr := string(saContent)

					// External SA namespace should be preserved
					Expect(saStr).To(ContainSubstring("namespace: external-namespace"),
						"external ServiceAccount namespace should be preserved")
					Expect(saStr).NotTo(ContainSubstring("namespace: {{ .Release.Namespace }}"),
						"external ServiceAccount should not use Release.Namespace")
				}
			}
			Expect(externalSAFound).To(BeTrue(), "external ServiceAccount file should exist")
		})

		It("should escape existing Go template syntax in CRD samples", func() {
			// Test a CRD with Go template syntax in default values.
			// Real-world example: gitops-promoter's ChangeTransferPolicy CRD has templates
			// in pullRequest.template fields that should be preserved as literal text.
			kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: test-project-system
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: changetransferpolicies.promoter.argoproj.io
  namespace: test-project-system
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
spec:
  group: promoter.argoproj.io
  names:
    kind: ChangeTransferPolicy
    listKind: ChangeTransferPolicyList
    plural: changetransferpolicies
    singular: changetransferpolicy
  scope: Namespaced
  versions:
  - name: v1alpha1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        description: ChangeTransferPolicy is the Schema for the changetransferpolicies API
        properties:
          spec:
            properties:
              activeBranch:
                type: string
              pullRequest:
                properties:
                  template:
                    properties:
                      description:
                        default: "Promoting {{ .ChangeTransferPolicy.Spec.ActiveBranch }}"
                        type: string
                      title:
                        default: "Promote {{ trunc 5 .ChangeTransferPolicy.Status.Proposed.Dry.Sha }}"
                        type: string
                    type: object
                type: object
            type: object
        type: object
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      serviceAccountName: test-project-controller-manager
      containers:
      - name: manager
        image: controller:latest
        args:
        - --metrics-bind-address=:8443
        - --health-probe-bind-address=:8081
`

			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying CRD file has escaped Go template syntax")
			crdDir := filepath.Join("dist", "chart", "templates", "crd")
			crdPath := filepath.Join(crdDir, "changetransferpolicies.promoter.argoproj.io.yaml")
			exists, err := afero.Exists(fs.FS, crdPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "CRD file should exist")

			crdContent, err := afero.ReadFile(fs.FS, crdPath)
			Expect(err).NotTo(HaveOccurred())
			crdStr := string(crdContent)

			// Existing Go template syntax should be escaped to prevent Helm from parsing it
			Expect(crdStr).To(ContainSubstring(`{{ "{{ .ChangeTransferPolicy.Spec.ActiveBranch }}" }}`),
				"existing template syntax should be escaped")
			Expect(crdStr).To(ContainSubstring(`{{ "{{ trunc 5 .ChangeTransferPolicy.Status.Proposed.Dry.Sha }}" }}`),
				"template functions should be escaped")

			// Verify we don't have unescaped template syntax that would break Helm rendering
			// We check that all ChangeTransferPolicy references are properly wrapped in escaped strings
			// Pattern checks for: default: "...{{ .ChangeTransferPolicy" (not escaped)
			// The properly escaped version is: default: "...{{ "{{ .ChangeTransferPolicy..." }}"
			Expect(crdStr).NotTo(MatchRegexp(`default:\s+"[^{]*\{\{\s*\.ChangeTransferPolicy`),
				"unescaped Go templates should not exist in default values")

			// Helm templates we add should still work (not escaped)
			Expect(crdStr).To(ContainSubstring("{{- if .Values.crd.enable }}"),
				"Helm conditional should be present and NOT escaped")
			Expect(crdStr).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"Helm namespace template should be present and NOT escaped")
			Expect(crdStr).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`),
				"Helm label template should be present and NOT escaped")
		})
	})

	Context("Custom Resource instances", func() {
		It("should ignore Custom Resource instances and not include them in the chart", func() {
			// This test validates that Custom Resources (CR instances, not CRDs) are
			// intentionally ignored and not included in the generated Helm chart.
			// CRs are environment-specific and should not be installed automatically.
			kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: test-project-system
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: cronjobs.batch.tutorial.kubebuilder.io
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
spec:
  group: batch.tutorial.kubebuilder.io
  names:
    kind: CronJob
    listKind: CronJobList
    plural: cronjobs
    singular: cronjob
  scope: Namespaced
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        description: CronJob is the Schema for the cronjobs API
        type: object
        properties:
          spec:
            type: object
            properties:
              schedule:
                type: string
---
apiVersion: batch.tutorial.kubebuilder.io/v1
kind: CronJob
metadata:
  labels:
    app.kubernetes.io/name: test-project
    app.kubernetes.io/managed-by: kustomize
  name: cronjob-sample
spec:
  schedule: "*/1 * * * *"
---
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
  name: custom-config
  namespace: test-project-system
data:
  key1: value1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      serviceAccountName: test-project-controller-manager
      containers:
      - name: manager
        image: controller:latest
`

			kustomizeFile := filepath.Join(tmpDir, "install.yaml")
			err := os.WriteFile(kustomizeFile, []byte(kustomizeYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser := kustomize.NewParser(kustomizeFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			By("verifying CRD and CR are correctly parsed")
			Expect(resources.CustomResourceDefinitions).To(HaveLen(1), "should have 1 CRD")
			Expect(resources.CustomResources).To(HaveLen(1), "should have 1 CR instance")
			Expect(resources.Other).To(HaveLen(1), "should have 1 other resource (ConfigMap)")

			By("verifying CR is a CronJob")
			cr := resources.CustomResources[0]
			Expect(cr.GetKind()).To(Equal("CronJob"))
			Expect(cr.GetAPIVersion()).To(Equal("batch.tutorial.kubebuilder.io/v1"))
			Expect(cr.GetName()).To(Equal("cronjob-sample"))

			converter := kustomize.NewChartConverter(resources, "test-project", "test-project", "dist")
			err = converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			By("verifying CR is NOT included in the chart (no samples directory)")
			samplesDir := filepath.Join("dist", "chart", "samples")
			exists, err := afero.Exists(fs.FS, samplesDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeFalse(), "samples directory should NOT exist - CRs are ignored")

			By("verifying CR is NOT in extras directory")
			extrasDir := filepath.Join("dist", "chart", "templates", "extras")
			exists, err = afero.Exists(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "extras directory should exist for ConfigMap")

			extrasFiles, err := afero.ReadDir(fs.FS, extrasDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(extrasFiles).To(HaveLen(1), "extras should only have ConfigMap, not CR")

			var configMapFound, crFound bool
			for _, f := range extrasFiles {
				if strings.Contains(f.Name(), "custom-config") {
					configMapFound = true
				}
				if strings.Contains(strings.ToLower(f.Name()), "cronjob") {
					crFound = true
				}
			}
			Expect(configMapFound).To(BeTrue(), "ConfigMap should be in extras")
			Expect(crFound).To(BeFalse(), "CR should NOT be in extras")

			By("verifying CRD is in crd directory")
			crdDir := filepath.Join("dist", "chart", "templates", "crd")
			exists, err = afero.Exists(fs.FS, crdDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue(), "crd directory should exist")

			crdFiles, err := afero.ReadDir(fs.FS, crdDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(crdFiles).To(HaveLen(1), "crd directory should have 1 CRD")
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/force_integration_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"os"
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"

	"sigs.k8s.io/kubebuilder/v4/pkg/config"
	cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("Force Flag Integration Test", func() {
	var (
		fs             machinery.Filesystem
		tmpDir         string
		manifestsFile  string
		outputDir      string
		projectConfig  config.Config
		scaffolderBase *editKustomizeScaffolder
	)

	BeforeEach(func() {
		var err error
		tmpDir, err = os.MkdirTemp("", "helm-force-test-*")
		Expect(err).NotTo(HaveOccurred())

		err = os.Chdir(tmpDir)
		Expect(err).NotTo(HaveOccurred())

		fs = machinery.Filesystem{
			FS: afero.NewBasePathFs(afero.NewOsFs(), tmpDir),
		}

		// Create PROJECT file
		projectConfig = cfgv3.New()
		projectConfig.SetProjectName("test-project")
		projectConfig.SetDomain("example.io")

		// Setup directories - use absolute path for manifestsFile since parser uses os.Open
		manifestsFile = filepath.Join(tmpDir, "dist", "install.yaml")
		outputDir = "dist"

		// Create minimal kustomize output file using real OS filesystem (parser uses os.Open)
		kustomizeYAML := `---
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
    control-plane: controller-manager
  name: test-project-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
    control-plane: controller-manager
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - name: manager
        image: controller:latest
        imagePullPolicy: IfNotPresent
        command:
        - /manager
        args:
        - --leader-elect
        - --health-probe-bind-address=:8081
        ports:
        - containerPort: 9443
          name: webhook-server
          protocol: TCP
        env:
        - name: TEST_ENV
          value: "test-value"
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
`

		// Use OS filesystem to write manifests file since Parser uses os.Open
		err = os.MkdirAll(filepath.Dir(manifestsFile), 0o755)
		Expect(err).NotTo(HaveOccurred())
		err = os.WriteFile(manifestsFile, []byte(kustomizeYAML), 0o644)
		Expect(err).NotTo(HaveOccurred())

		scaffolderBase = &editKustomizeScaffolder{
			config:        projectConfig,
			fs:            fs,
			manifestsFile: manifestsFile,
			outputDir:     outputDir,
		}
	})

	AfterEach(func() {
		if tmpDir != "" {
			_ = os.RemoveAll(tmpDir)
		}
	})

	Context("when --force flag is NOT used", func() {
		It("should NOT overwrite existing Chart.yaml, values.yaml, .helmignore, _helpers.tpl, and test-chart.yml", func() {
			// First generation with force=false
			scaffolderBase.force = false
			err := scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			// Define file paths (absolute paths for OS filesystem)
			chartPath := filepath.Join(tmpDir, outputDir, "chart", "Chart.yaml")
			valuesPath := filepath.Join(tmpDir, outputDir, "chart", "values.yaml")
			helmignorePath := filepath.Join(tmpDir, outputDir, "chart", ".helmignore")
			helpersPath := filepath.Join(tmpDir, outputDir, "chart", "templates", "_helpers.tpl")
			testChartPath := filepath.Join(tmpDir, ".github", "workflows", "test-chart.yml")

			// Verify files exist
			_, err = os.ReadFile(chartPath)
			Expect(err).NotTo(HaveOccurred())
			_, err = os.ReadFile(valuesPath)
			Expect(err).NotTo(HaveOccurred())
			_, err = os.ReadFile(helmignorePath)
			Expect(err).NotTo(HaveOccurred())
			_, err = os.ReadFile(helpersPath)
			Expect(err).NotTo(HaveOccurred())
			_, err = os.ReadFile(testChartPath)
			Expect(err).NotTo(HaveOccurred())

			// Modify all protected files
			customChartContent := "# CUSTOM CHART YAML\nversion: 999.0.0\n"
			customValuesContent := "# CUSTOM VALUES YAML\ncustom: value\n"
			customHelmignoreContent := "# CUSTOM HELMIGNORE\n*.custom\n"
			customHelpersContent := "# CUSTOM HELPERS TPL\n{{/* custom helper */}}\n"
			customTestChartContent := "# CUSTOM TEST CHART\nname: Custom Workflow\n"

			err = os.WriteFile(chartPath, []byte(customChartContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(valuesPath, []byte(customValuesContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(helmignorePath, []byte(customHelmignoreContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(helpersPath, []byte(customHelpersContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(testChartPath, []byte(customTestChartContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			// Second generation with force=false
			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			// Verify all protected files were NOT overwritten
			chartContent, err := os.ReadFile(chartPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(chartContent)).To(Equal(customChartContent), "Chart.yaml should not be overwritten without --force")

			valuesContent, err := os.ReadFile(valuesPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(valuesContent)).To(Equal(customValuesContent), "values.yaml should not be overwritten without --force")

			helmignoreContent, err := os.ReadFile(helmignorePath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(helmignoreContent)).To(Equal(customHelmignoreContent), ".helmignore should not be overwritten without --force")

			helpersContent, err := os.ReadFile(helpersPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(helpersContent)).To(Equal(customHelpersContent), "_helpers.tpl should not be overwritten without --force")

			testChartContent, err := os.ReadFile(testChartPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(testChartContent)).To(Equal(customTestChartContent), "test-chart.yml should not be overwritten without --force")
		})
	})

	Context("when --force flag IS used", func() {
		It("should overwrite all files EXCEPT Chart.yaml (which is never overwritten)", func() {
			// First generation with force=false
			scaffolderBase.force = false
			err := scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			// Define file paths (absolute paths for OS filesystem)
			chartPath := filepath.Join(tmpDir, outputDir, "chart", "Chart.yaml")
			valuesPath := filepath.Join(tmpDir, outputDir, "chart", "values.yaml")
			helmignorePath := filepath.Join(tmpDir, outputDir, "chart", ".helmignore")
			helpersPath := filepath.Join(tmpDir, outputDir, "chart", "templates", "_helpers.tpl")
			testChartPath := filepath.Join(tmpDir, ".github", "workflows", "test-chart.yml")

			// Modify all protected files with custom content
			customChartContent := "# CUSTOM CHART YAML\nversion: 999.0.0\n"
			customValuesContent := "# CUSTOM VALUES YAML\ncustom: value\n"
			customHelmignoreContent := "# CUSTOM HELMIGNORE\n*.custom\n"
			customHelpersContent := "# CUSTOM HELPERS TPL\n{{/* custom helper */}}\n"
			customTestChartContent := "# CUSTOM TEST CHART\nname: Custom Workflow\n"

			err = os.WriteFile(chartPath, []byte(customChartContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(valuesPath, []byte(customValuesContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(helmignorePath, []byte(customHelmignoreContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(helpersPath, []byte(customHelpersContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(testChartPath, []byte(customTestChartContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			// Second generation with force=true
			scaffolderBase.force = true
			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			// Verify Chart.yaml was NOT overwritten (never overwritten, even with --force)
			chartContent, err := os.ReadFile(chartPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(chartContent)).To(Equal(customChartContent), "Chart.yaml should NEVER be overwritten, even with --force")

			valuesContent, err := os.ReadFile(valuesPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(valuesContent)).NotTo(Equal(customValuesContent), "values.yaml should be overwritten with --force")
			Expect(string(valuesContent)).To(ContainSubstring("manager:"), "values.yaml should contain manager section")

			helmignoreContent, err := os.ReadFile(helmignorePath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(helmignoreContent)).NotTo(Equal(customHelmignoreContent), ".helmignore should be overwritten with --force")
			Expect(string(helmignoreContent)).To(ContainSubstring(".DS_Store"), ".helmignore should contain default patterns")

			helpersContent, err := os.ReadFile(helpersPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(helpersContent)).NotTo(Equal(customHelpersContent), "_helpers.tpl should be overwritten with --force")
			Expect(string(helpersContent)).To(ContainSubstring("test-project.name"), "_helpers.tpl should contain template helpers")

			testChartContent, err := os.ReadFile(testChartPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(testChartContent)).NotTo(Equal(customTestChartContent), "test-chart.yml should be overwritten with --force")
			Expect(string(testChartContent)).To(ContainSubstring("Test Chart"), "test-chart.yml should contain workflow name")
		})

		It("should overwrite files on first run when force=true", func() {
			// Create pre-existing custom files before any scaffold run (absolute paths)
			chartPath := filepath.Join(tmpDir, outputDir, "chart", "Chart.yaml")
			valuesPath := filepath.Join(tmpDir, outputDir, "chart", "values.yaml")

			err := os.MkdirAll(filepath.Dir(chartPath), 0o755)
			Expect(err).NotTo(HaveOccurred())

			customChartContent := "# PRE-EXISTING CHART\nversion: 0.0.1\n"
			customValuesContent := "# PRE-EXISTING VALUES\nold: data\n"

			err = os.WriteFile(chartPath, []byte(customChartContent), 0o644)
			Expect(err).NotTo(HaveOccurred())
			err = os.WriteFile(valuesPath, []byte(customValuesContent), 0o644)
			Expect(err).NotTo(HaveOccurred())

			// First generation with force=true should overwrite
			scaffolderBase.force = true
			err = scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			// Verify Chart.yaml was NOT overwritten (never overwritten, even on first run with force)
			chartContent, err := os.ReadFile(chartPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(chartContent)).To(Equal(customChartContent), "Chart.yaml should NEVER be overwritten, even on first run with --force")

			// Verify values.yaml WAS overwritten
			valuesContent, err := os.ReadFile(valuesPath)
			Expect(err).NotTo(HaveOccurred())
			Expect(string(valuesContent)).NotTo(Equal(customValuesContent), "values.yaml should be overwritten on first run with --force")
			Expect(string(valuesContent)).To(ContainSubstring("manager:"))
		})
	})

	Context("when template files are modified", func() {
		It("should verify template files exist in templates/ directory", func() {
			// First generation
			scaffolderBase.force = false
			err := scaffolderBase.Scaffold()
			Expect(err).NotTo(HaveOccurred())

			// Verify that template directory was created with files
			templatesDir := filepath.Join(tmpDir, outputDir, "chart", "templates")
			info, err := os.Stat(templatesDir)
			Expect(err).NotTo(HaveOccurred(), "Templates directory should be created")
			Expect(info.IsDir()).To(BeTrue(), "Templates should be a directory")

			// At minimum, _helpers.tpl should exist
			helpersPath := filepath.Join(templatesDir, "_helpers.tpl")
			_, err = os.Stat(helpersPath)
			Expect(err).NotTo(HaveOccurred(), "_helpers.tpl should exist in templates/")
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"fmt"
	"strconv"
	"strings"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

// ChartConverter orchestrates the conversion of kustomize output to Helm chart templates
type ChartConverter struct {
	resources *ParsedResources
	// The actual namePrefix detected from kustomize resources
	detectedPrefix string
	// The chart name used for template namespacing
	chartName string
	outputDir string

	// Components for conversion
	organizer *ResourceOrganizer
	templater *HelmTemplater
	writer    *ChartWriter
}

// NewChartConverter creates a new chart converter with all necessary components
func NewChartConverter(resources *ParsedResources, detectedPrefix, chartName, outputDir string) *ChartConverter {
	// Extract manager namespace from Deployment, default to -system
	managerNamespace := detectedPrefix + "-system"
	if resources.Deployment != nil {
		if ns := resources.Deployment.GetNamespace(); ns != "" {
			managerNamespace = ns
		}
	}

	organizer := NewResourceOrganizer(resources)
	templater := NewHelmTemplater(detectedPrefix, chartName, managerNamespace)
	writer := NewChartWriter(templater, outputDir, managerNamespace)

	return &ChartConverter{
		resources:      resources,
		detectedPrefix: detectedPrefix,
		chartName:      chartName,
		outputDir:      outputDir,
		organizer:      organizer,
		templater:      templater,
		writer:         writer,
	}
}

// WriteChartFiles converts all resources to Helm chart templates and writes them to the filesystem
func (c *ChartConverter) WriteChartFiles(fs machinery.Filesystem) error {
	// Organize resources by their logical function
	resourceGroups := c.organizer.OrganizeByFunction()

	// Write each group to appropriate template files
	for groupName, resources := range resourceGroups {
		if len(resources) > 0 {
			// De-duplicate exact resources by (apiVersion, kind, namespace, name)
			deduped := dedupeResources(resources)
			if err := c.writer.WriteResourceGroup(fs, groupName, deduped); err != nil {
				return fmt.Errorf("failed to write %s resources: %w", groupName, err)
			}
		}
	}

	return nil
}

// dedupeResources removes exact duplicate resources by keying on
// apiVersion, kind, namespace (optional), and name.
func dedupeResources(resources []*unstructured.Unstructured) []*unstructured.Unstructured {
	seen := make(map[string]struct{})
	out := make([]*unstructured.Unstructured, 0, len(resources))
	for _, r := range resources {
		if r == nil {
			continue
		}
		key := r.GetAPIVersion() + "|" + r.GetKind() + "|" + r.GetNamespace() + "|" + r.GetName()
		if _, exists := seen[key]; exists {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, r)
	}
	return out
}

// ExtractDeploymentConfig extracts configuration values from the deployment for values.yaml
func (c *ChartConverter) ExtractDeploymentConfig() map[string]any {
	if c.resources.Deployment == nil {
		return make(map[string]any)
	}

	config := make(map[string]any)
	specMap := extractDeploymentSpec(c.resources.Deployment)
	if specMap == nil {
		return config
	}

	extractPodSecurityContext(specMap, config)
	extractImagePullSecrets(specMap, config)
	extractPodNodeSelector(specMap, config)
	extractPodTolerations(specMap, config)
	extractPodAffinity(specMap, config)

	container := firstManagerContainer(specMap)
	if container == nil {
		return config
	}

	extractContainerEnv(container, config)
	extractContainerImage(container, config)
	extractContainerArgs(container, config)
	extractContainerPorts(container, config)
	extractContainerResources(container, config)
	extractContainerSecurityContext(container, config)

	return config
}

func extractDeploymentSpec(deployment *unstructured.Unstructured) map[string]any {
	spec, found, err := unstructured.NestedFieldNoCopy(deployment.Object, "spec", "template", "spec")
	if !found || err != nil {
		return nil
	}

	specMap, ok := spec.(map[string]any)
	if !ok {
		return nil
	}

	return specMap
}

func extractImagePullSecrets(specMap map[string]any, config map[string]any) {
	imagePullSecrets, found, err := unstructured.NestedFieldNoCopy(specMap, "imagePullSecrets")
	if !found || err != nil {
		return
	}

	imagePullSecretsList, ok := imagePullSecrets.([]any)
	if !ok || len(imagePullSecretsList) == 0 {
		return
	}

	config["imagePullSecrets"] = imagePullSecretsList
}

func extractPodSecurityContext(specMap map[string]any, config map[string]any) {
	podSecurityContext, found, err := unstructured.NestedFieldNoCopy(specMap, "securityContext")
	if !found || err != nil {
		return
	}

	podSecMap, ok := podSecurityContext.(map[string]any)
	if !ok || len(podSecMap) == 0 {
		return
	}

	config["podSecurityContext"] = podSecurityContext
}

func extractPodNodeSelector(specMap map[string]any, config map[string]any) {
	raw, found, err := unstructured.NestedFieldNoCopy(specMap, "nodeSelector")
	if !found || err != nil {
		return
	}

	result, ok := raw.(map[string]any)
	if !ok || len(result) == 0 {
		return
	}

	config["podNodeSelector"] = result
}

func extractPodTolerations(specMap map[string]any, config map[string]any) {
	raw, found, err := unstructured.NestedFieldNoCopy(specMap, "tolerations")
	if !found || err != nil {
		return
	}

	result, ok := raw.([]any)
	if !ok || len(result) == 0 {
		return
	}

	config["podTolerations"] = result
}

func extractPodAffinity(specMap map[string]any, config map[string]any) {
	raw, found, err := unstructured.NestedFieldNoCopy(specMap, "affinity")
	if !found || err != nil {
		return
	}

	result, ok := raw.(map[string]any)
	if !ok || len(result) == 0 {
		return
	}

	config["podAffinity"] = result
}

func firstManagerContainer(specMap map[string]any) map[string]any {
	containers, found, err := unstructured.NestedFieldNoCopy(specMap, "containers")
	if !found || err != nil {
		return nil
	}

	containersList, ok := containers.([]any)
	if !ok || len(containersList) == 0 {
		return nil
	}

	firstContainer, ok := containersList[0].(map[string]any)
	if !ok {
		return nil
	}

	return firstContainer
}

func extractContainerEnv(container map[string]any, config map[string]any) {
	env, found, err := unstructured.NestedFieldNoCopy(container, "env")
	if !found || err != nil {
		return
	}

	envList, ok := env.([]any)
	if !ok || len(envList) == 0 {
		return
	}

	config["env"] = envList
}

func extractContainerImage(container map[string]any, config map[string]any) {
	imageValue, found, err := unstructured.NestedString(container, "image")
	if !found || err != nil || imageValue == "" {
		return
	}

	repository := imageValue
	tag := "latest"
	lastColon := strings.LastIndex(imageValue, ":")
	lastSlash := strings.LastIndex(imageValue, "/")
	if lastColon != -1 && lastColon > lastSlash {
		repository = imageValue[:lastColon]
		if lastColon+1 < len(imageValue) {
			tag = imageValue[lastColon+1:]
		}
	}

	pullPolicy, _, err := unstructured.NestedString(container, "imagePullPolicy")
	if err != nil || pullPolicy == "" {
		pullPolicy = "IfNotPresent"
	}

	config["image"] = map[string]any{
		"repository": repository,
		"tag":        tag,
		"pullPolicy": pullPolicy,
	}
}

func extractContainerArgs(container map[string]any, config map[string]any) {
	args, found, err := unstructured.NestedFieldNoCopy(container, "args")
	if !found || err != nil {
		return
	}

	argsList, ok := args.([]any)
	if !ok || len(argsList) == 0 {
		return
	}

	filteredArgs := make([]any, 0, len(argsList))
	for _, rawArg := range argsList {
		strArg, ok := rawArg.(string)
		if !ok {
			filteredArgs = append(filteredArgs, rawArg)
			continue
		}

		// Extract port values from bind-address arguments and store them
		// These arguments should not be exposed under args because they will be
		// reconstructed from the port values in values.yaml
		if strings.Contains(strArg, "--metrics-bind-address") {
			if port := extractPortFromArg(strArg); port > 0 {
				if _, exists := config["metricsPort"]; !exists {
					config["metricsPort"] = port
				}
			}
			continue
		}
		if strings.Contains(strArg, "--health-probe-bind-address") {
			continue
		}
		if strings.Contains(strArg, "--webhook-cert-path") ||
			strings.Contains(strArg, "--metrics-cert-path") {
			continue
		}
		filteredArgs = append(filteredArgs, strArg)
	}

	if len(filteredArgs) > 0 {
		config["args"] = filteredArgs
	}
}

// extractPortFromArg extracts port number from arguments like "--metrics-bind-address=:8443"
func extractPortFromArg(arg string) int {
	// Handle formats: --flag=:8443, --flag=0.0.0.0:8443, etc.
	parts := strings.Split(arg, "=")
	if len(parts) != 2 {
		return 0
	}

	portPart := parts[1]
	// Remove leading : or host part
	if idx := strings.LastIndex(portPart, ":"); idx != -1 {
		portPart = portPart[idx+1:]
	}

	port, err := strconv.Atoi(portPart)
	if err != nil || port <= 0 || port > 65535 {
		return 0
	}
	return port
}

// extractContainerPorts extracts port configurations from container ports
func extractContainerPorts(container map[string]any, config map[string]any) {
	// Use NestedFieldNoCopy to avoid deep copy issues with int values
	portsField, found, err := unstructured.NestedFieldNoCopy(container, "ports")
	if !found || err != nil {
		return
	}

	ports, ok := portsField.([]any)
	if !ok {
		return
	}

	for _, p := range ports {
		portMap, ok := p.(map[string]any)
		if !ok {
			continue
		}

		name, _ := portMap["name"].(string)
		var containerPort int

		// Try int64 first (from YAML unmarshaling)
		if cp, ok := portMap["containerPort"].(int64); ok {
			containerPort = int(cp)
		} else if cp, ok := portMap["containerPort"].(int); ok {
			containerPort = cp
		} else {
			continue
		}

		// Look for webhook-server port
		if name == "webhook-server" || strings.Contains(name, "webhook") {
			if _, exists := config["webhookPort"]; !exists {
				config["webhookPort"] = containerPort
			}
		}
	}
}

func extractContainerResources(container map[string]any, config map[string]any) {
	resources, found, err := unstructured.NestedFieldNoCopy(container, "resources")
	if !found || err != nil {
		return
	}

	resourcesMap, ok := resources.(map[string]any)
	if !ok || len(resourcesMap) == 0 {
		return
	}

	config["resources"] = resources
}

func extractContainerSecurityContext(container map[string]any, config map[string]any) {
	securityContext, found, err := unstructured.NestedFieldNoCopy(container, "securityContext")
	if !found || err != nil {
		return
	}

	secMap, ok := securityContext.(map[string]any)
	if !ok || len(secMap) == 0 {
		return
	}

	config["securityContext"] = securityContext
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_converter_test.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"github.com/spf13/afero"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("ChartConverter", func() {
	var (
		converter *ChartConverter
		resources *ParsedResources
		fs        machinery.Filesystem
	)

	BeforeEach(func() {
		// Create test resources
		resources = &ParsedResources{}

		// Add a test deployment
		deployment := &unstructured.Unstructured{}
		deployment.SetAPIVersion("apps/v1")
		deployment.SetKind("Deployment")
		deployment.SetName("test-controller")
		deployment.SetNamespace("test-system")

		// Set deployment spec
		err := unstructured.SetNestedField(deployment.Object, int64(1), "spec", "replicas")
		Expect(err).NotTo(HaveOccurred())

		resources.Deployment = deployment

		// Create filesystem
		fs = machinery.Filesystem{FS: afero.NewMemMapFs()}

		// Create converter
		converter = NewChartConverter(resources, "test-project", "test-project", "dist")
	})

	Context("NewChartConverter", func() {
		It("should create a converter with correct properties", func() {
			Expect(converter.resources).To(Equal(resources))
			Expect(converter.detectedPrefix).To(Equal("test-project"))
			Expect(converter.outputDir).To(Equal("dist"))
		})
	})

	Context("WriteChartFiles", func() {
		It("should write chart files to filesystem", func() {
			// Add some resources to test with
			serviceAccount := &unstructured.Unstructured{}
			serviceAccount.SetAPIVersion("v1")
			serviceAccount.SetKind("ServiceAccount")
			serviceAccount.SetName("test-sa")
			serviceAccount.SetNamespace("test-system")
			resources.ServiceAccount = serviceAccount

			// Add RBAC resources to test rbac directory creation
			clusterRole := &unstructured.Unstructured{}
			clusterRole.SetAPIVersion("rbac.authorization.k8s.io/v1")
			clusterRole.SetKind("ClusterRole")
			clusterRole.SetName("test-role")
			resources.ClusterRoles = []*unstructured.Unstructured{clusterRole}

			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			exists, err := afero.Exists(fs.FS, "dist/chart/templates/manager")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())

			exists, err = afero.Exists(fs.FS, "dist/chart/templates/rbac")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())
		})

		It("should deduplicate identical resources within a group", func() {
			// Prepare two identical Services in the metrics group
			metricsSvc1 := &unstructured.Unstructured{}
			metricsSvc1.SetAPIVersion("v1")
			metricsSvc1.SetKind("Service")
			metricsSvc1.SetName("test-project-controller-manager-metrics-service")
			metricsSvc1.SetNamespace("test-system")

			metricsSvc2 := &unstructured.Unstructured{}
			metricsSvc2.SetAPIVersion("v1")
			metricsSvc2.SetKind("Service")
			metricsSvc2.SetName("test-project-controller-manager-metrics-service")
			metricsSvc2.SetNamespace("test-system")

			// Add both to resources; organizer will place them into the metrics group
			resources.Services = append(resources.Services, metricsSvc1, metricsSvc2)

			// Write chart files
			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			// Expect only one file to be written for the metrics service after de-duplication
			metricsDir := filepath.Join("dist", "chart", "templates", "metrics")
			files, err := afero.ReadDir(fs.FS, metricsDir)
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(1), "expected only one metrics service file after deduplication")
		})
	})

	Context("ExtractDeploymentConfig", func() {
		It("should extract deployment configuration correctly", func() {
			// Set up deployment with environment variables
			containers := []any{
				map[string]any{
					"name":            "manager",
					"image":           "controller:latest",
					"imagePullPolicy": "IfNotPresent",
					"args": []any{
						"--metrics-bind-address=:8443",
						"--leader-elect",
						"--custom-flag=value",
						"--health-probe-bind-address=:8081",
						"--webhook-cert-path=/tmp/k8s-webhook-server/serving-certs",
					},
					"env": []any{
						map[string]any{
							"name":  "TEST_ENV",
							"value": "test-value",
						},
					},
					"resources": map[string]any{
						"limits": map[string]any{
							"cpu":    "100m",
							"memory": "128Mi",
						},
					},
				},
			}

			err := unstructured.SetNestedSlice(
				resources.Deployment.Object,
				containers,
				"spec", "template", "spec", "containers",
			)
			Expect(err).NotTo(HaveOccurred())

			config := converter.ExtractDeploymentConfig()

			Expect(config).NotTo(BeNil())
			Expect(config).To(HaveKey("env"))
			Expect(config).To(HaveKey("image"))
			Expect(config).To(HaveKey("resources"))
			Expect(config).To(HaveKey("args"))

			imageConfig, ok := config["image"].(map[string]any)
			Expect(ok).To(BeTrue())
			Expect(imageConfig["repository"]).To(Equal("controller"))
			Expect(imageConfig["tag"]).To(Equal("latest"))
			Expect(imageConfig["pullPolicy"]).To(Equal("IfNotPresent"))

			args, ok := config["args"].([]any)
			Expect(ok).To(BeTrue())
			Expect(args).To(ContainElement("--leader-elect"))
			Expect(args).To(ContainElement("--custom-flag=value"))
			Expect(args).NotTo(ContainElement("--metrics-bind-address=:8443"))
			Expect(args).NotTo(ContainElement("--health-probe-bind-address=:8081"))
		})

		It("should extract port configurations from args", func() {
			// Set up deployment with port-related args
			containers := []any{
				map[string]any{
					"name":  "manager",
					"image": "controller:latest",
					"args": []any{
						"--metrics-bind-address=:8443",
						"--health-probe-bind-address=:8081",
						"--leader-elect",
					},
				},
			}

			err := unstructured.SetNestedSlice(
				resources.Deployment.Object,
				containers,
				"spec", "template", "spec", "containers",
			)
			Expect(err).NotTo(HaveOccurred())

			config := converter.ExtractDeploymentConfig()

			Expect(config).To(HaveKey("metricsPort"))
			Expect(config["metricsPort"]).To(Equal(8443))
			Expect(config).NotTo(HaveKey("healthPort"))
		})

		It("should extract webhook port from container ports", func() {
			// Set up deployment with webhook container port
			containers := []any{
				map[string]any{
					"name":  "manager",
					"image": "controller:latest",
					"ports": []any{
						map[string]any{
							"containerPort": int64(9443),
							"name":          "webhook-server",
							"protocol":      "TCP",
						},
					},
				},
			}

			err := unstructured.SetNestedSlice(
				resources.Deployment.Object,
				containers,
				"spec", "template", "spec", "containers",
			)
			Expect(err).NotTo(HaveOccurred())

			config := converter.ExtractDeploymentConfig()

			Expect(config).To(HaveKey("webhookPort"))
			Expect(config["webhookPort"]).To(Equal(9443))
		})

		It("should extract custom port values", func() {
			// Set up deployment with custom ports
			containers := []any{
				map[string]any{
					"name":  "manager",
					"image": "controller:latest",
					"args": []any{
						"--metrics-bind-address=:9090",
						"--health-probe-bind-address=:9091",
					},
					"ports": []any{
						map[string]any{
							"containerPort": int64(9444),
							"name":          "webhook-server",
							"protocol":      "TCP",
						},
					},
				},
			}

			err := unstructured.SetNestedSlice(
				resources.Deployment.Object,
				containers,
				"spec", "template", "spec", "containers",
			)
			Expect(err).NotTo(HaveOccurred())

			config := converter.ExtractDeploymentConfig()

			Expect(config["metricsPort"]).To(Equal(9090))
			Expect(config["healthPort"]).To(BeNil())
			Expect(config["webhookPort"]).To(Equal(9444))
		})

		It("should extract imagePullSecrets", func() {
			// Set up deployment with image pull secrets
			containers := []any{
				map[string]any{
					"name":  "manager",
					"image": "controller:latest",
				},
			}
			imagePullSecrets := []any{
				map[string]any{
					"name": "test-secret",
				},
			}
			// Set the image pull secrets
			err := unstructured.SetNestedSlice(
				resources.Deployment.Object,
				imagePullSecrets,
				"spec", "template", "spec", "imagePullSecrets",
			)
			Expect(err).NotTo(HaveOccurred())
			// Set the containers
			err = unstructured.SetNestedSlice(
				resources.Deployment.Object,
				containers,
				"spec", "template", "spec", "containers",
			)
			Expect(err).NotTo(HaveOccurred())

			config := converter.ExtractDeploymentConfig()
			Expect(config).To(HaveKey("imagePullSecrets"))
			Expect(config["imagePullSecrets"]).To(Equal(imagePullSecrets))
		})

		It("should handle deployment without containers", func() {
			config := converter.ExtractDeploymentConfig()
			Expect(config).To(BeEmpty())
		})
	})

	Context("extractPortFromArg", func() {
		It("should extract port from :PORT format", func() {
			port := extractPortFromArg("--metrics-bind-address=:8443")
			Expect(port).To(Equal(8443))
		})

		It("should extract port from 0.0.0.0:PORT format", func() {
			port := extractPortFromArg("--metrics-bind-address=0.0.0.0:8443")
			Expect(port).To(Equal(8443))
		})

		It("should extract port from HOST:PORT format", func() {
			port := extractPortFromArg("--health-probe-bind-address=localhost:8081")
			Expect(port).To(Equal(8081))
		})

		It("should return 0 for invalid formats", func() {
			port := extractPortFromArg("--invalid-arg")
			Expect(port).To(Equal(0))

			port = extractPortFromArg("--no-equals:8443")
			Expect(port).To(Equal(0))

			port = extractPortFromArg("--port=invalid")
			Expect(port).To(Equal(0))
		})

		It("should return 0 for out-of-range ports", func() {
			port := extractPortFromArg("--port=:0")
			Expect(port).To(Equal(0))

			port = extractPortFromArg("--port=:99999")
			Expect(port).To(Equal(0))
		})
	})

	Context("Extras Directory", func() {
		It("should place ConfigMap in extras directory", func() {
			// Create a ConfigMap that doesn't fit standard categories
			configMap := &unstructured.Unstructured{}
			configMap.SetAPIVersion("v1")
			configMap.SetKind("ConfigMap")
			configMap.SetName("custom-config")
			configMap.SetNamespace("test-system")
			configMap.Object["metadata"] = map[string]any{
				"name":      "custom-config",
				"namespace": "test-system",
				"labels": map[string]any{
					"app.kubernetes.io/name":       "test-project",
					"app.kubernetes.io/managed-by": "kustomize",
				},
			}
			configMap.Object["data"] = map[string]any{
				"key": "value",
			}

			resources.Other = []*unstructured.Unstructured{configMap}

			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			// Verify extras directory was created
			exists, err := afero.Exists(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())

			// Verify ConfigMap file was created
			files, err := afero.ReadDir(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(1))
			Expect(files[0].Name()).To(ContainSubstring("custom-config"))

			// Read the ConfigMap file and verify it has Helm templating
			content, err := afero.ReadFile(fs.FS, filepath.Join("dist/chart/templates/extras", files[0].Name()))
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)

			// Verify Helm templates are applied
			Expect(contentStr).To(ContainSubstring("{{ .Release.Namespace }}"))
			Expect(contentStr).To(ContainSubstring("app.kubernetes.io/name:"))
			Expect(contentStr).To(ContainSubstring("app.kubernetes.io/managed-by:"))
		})

		It("should place custom Service in extras directory", func() {
			// Create a custom Service that is neither webhook nor metrics
			customService := &unstructured.Unstructured{}
			customService.SetAPIVersion("v1")
			customService.SetKind("Service")
			customService.SetName("custom-service")
			customService.SetNamespace("test-project-system")
			customService.Object["metadata"] = map[string]any{
				"name":      "custom-service",
				"namespace": "test-project-system",
				"labels": map[string]any{
					"app.kubernetes.io/name":       "test-project",
					"app.kubernetes.io/managed-by": "kustomize",
				},
			}
			customService.Object["spec"] = map[string]any{
				"ports": []any{
					map[string]any{
						"port":       8080,
						"targetPort": 8080,
					},
				},
			}

			resources.Services = []*unstructured.Unstructured{customService}

			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			// Verify extras directory was created
			exists, err := afero.Exists(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())

			// Verify Service file was created in extras
			files, err := afero.ReadDir(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(1))
			Expect(files[0].Name()).To(ContainSubstring("custom-service"))
		})

		It("should place Secret in extras directory", func() {
			// Create a Secret
			secret := &unstructured.Unstructured{}
			secret.SetAPIVersion("v1")
			secret.SetKind("Secret")
			secret.SetName("custom-secret")
			secret.SetNamespace("test-system")
			secret.Object["metadata"] = map[string]any{
				"name":      "custom-secret",
				"namespace": "test-system",
				"labels": map[string]any{
					"app.kubernetes.io/name":       "test-project",
					"app.kubernetes.io/managed-by": "kustomize",
				},
			}
			secret.Object["data"] = map[string]any{
				"password": "c2VjcmV0",
			}

			resources.Other = []*unstructured.Unstructured{secret}

			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			// Verify extras directory was created
			exists, err := afero.Exists(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())

			// Verify Secret file was created
			files, err := afero.ReadDir(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(1))
			Expect(files[0].Name()).To(ContainSubstring("custom-secret"))

			// Read the Secret file and verify it has Helm templating
			content, err := afero.ReadFile(fs.FS, filepath.Join("dist/chart/templates/extras", files[0].Name()))
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)

			// Verify Helm templates are applied
			Expect(contentStr).To(ContainSubstring("{{ .Release.Namespace }}"))
		})

		It("should handle multiple extras resources", func() {
			// Create multiple extras resources
			configMap := &unstructured.Unstructured{}
			configMap.SetAPIVersion("v1")
			configMap.SetKind("ConfigMap")
			configMap.SetName("config1")
			configMap.SetNamespace("test-project-system")
			configMap.Object["metadata"] = map[string]any{
				"name":      "config1",
				"namespace": "test-project-system",
			}

			secret := &unstructured.Unstructured{}
			secret.SetAPIVersion("v1")
			secret.SetKind("Secret")
			secret.SetName("secret1")
			secret.SetNamespace("test-project-system")
			secret.Object["metadata"] = map[string]any{
				"name":      "secret1",
				"namespace": "test-project-system",
			}

			customService := &unstructured.Unstructured{}
			customService.SetAPIVersion("v1")
			customService.SetKind("Service")
			customService.SetName("custom-svc")
			customService.SetNamespace("test-project-system")
			customService.Object["metadata"] = map[string]any{
				"name":      "custom-svc",
				"namespace": "test-project-system",
			}

			resources.Other = []*unstructured.Unstructured{configMap, secret}
			resources.Services = []*unstructured.Unstructured{customService}

			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			// Verify all three files were created
			files, err := afero.ReadDir(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(3))
		})

		It("should apply standard Helm labels to extras resources", func() {
			// Create a ConfigMap
			configMap := &unstructured.Unstructured{}
			configMap.SetAPIVersion("v1")
			configMap.SetKind("ConfigMap")
			configMap.SetName("test-config")
			configMap.SetNamespace("test-system")
			configMap.Object["metadata"] = map[string]any{
				"name":      "test-config",
				"namespace": "test-system",
				"labels": map[string]any{
					"app.kubernetes.io/name":       "test-project",
					"app.kubernetes.io/managed-by": "kustomize",
				},
			}

			resources.Other = []*unstructured.Unstructured{configMap}

			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			// Read the ConfigMap file
			files, err := afero.ReadDir(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(files).To(HaveLen(1))

			content, err := afero.ReadFile(fs.FS, filepath.Join("dist/chart/templates/extras", files[0].Name()))
			Expect(err).NotTo(HaveOccurred())
			contentStr := string(content)

			// Verify all standard Helm labels are present
			Expect(contentStr).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(contentStr).To(ContainSubstring("app.kubernetes.io/instance: {{ .Release.Name }}"))
			Expect(contentStr).To(ContainSubstring("app.kubernetes.io/managed-by: {{ .Release.Service }}"))
			Expect(contentStr).To(ContainSubstring(
				`helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}`))
		})

		It("should not place webhook or metrics services in extras", func() {
			// Create webhook service
			webhookService := &unstructured.Unstructured{}
			webhookService.SetAPIVersion("v1")
			webhookService.SetKind("Service")
			webhookService.SetName("test-project-webhook-service")
			webhookService.SetNamespace("test-project-system")
			webhookService.Object["metadata"] = map[string]any{
				"name":      "test-project-webhook-service",
				"namespace": "test-project-system",
			}

			// Create metrics service
			metricsService := &unstructured.Unstructured{}
			metricsService.SetAPIVersion("v1")
			metricsService.SetKind("Service")
			metricsService.SetName("test-project-controller-manager-metrics-service")
			metricsService.SetNamespace("test-project-system")
			metricsService.Object["metadata"] = map[string]any{
				"name":      "test-project-controller-manager-metrics-service",
				"namespace": "test-project-system",
			}

			resources.Services = []*unstructured.Unstructured{webhookService, metricsService}

			err := converter.WriteChartFiles(fs)
			Expect(err).NotTo(HaveOccurred())

			// Verify extras directory was not created (webhook/metrics go to their own dirs)
			exists, err := afero.Exists(fs.FS, "dist/chart/templates/extras")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeFalse())

			// Verify webhook directory was created
			exists, err = afero.Exists(fs.FS, "dist/chart/templates/webhook")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())

			// Verify metrics directory was created
			exists, err = afero.Exists(fs.FS, "dist/chart/templates/metrics")
			Expect(err).NotTo(HaveOccurred())
			Expect(exists).To(BeTrue())
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/chart_writer.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"bytes"
	"fmt"
	"path/filepath"
	"strings"

	"github.com/spf13/afero"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"sigs.k8s.io/yaml"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

// ChartWriter handles writing Helm chart template files
type ChartWriter struct {
	templater        *HelmTemplater
	outputDir        string
	managerNamespace string
}

// NewChartWriter creates a new chart writer
func NewChartWriter(templater *HelmTemplater, outputDir string, managerNamespace string) *ChartWriter {
	return &ChartWriter{
		templater:        templater,
		outputDir:        outputDir,
		managerNamespace: managerNamespace,
	}
}

// WriteResourceGroup writes a group of resources to a Helm template file
func (w *ChartWriter) WriteResourceGroup(
	fs machinery.Filesystem, groupName string,
	resources []*unstructured.Unstructured,
) error {
	// Special handling for namespace - write as single file
	if groupName == "namespace" {
		return w.writeNamespaceFile(fs, resources[0])
	}

	// For CRDs, certificates, and other resources that should be split, write individual files
	if w.shouldSplitFiles(groupName) {
		return w.writeSplitFiles(fs, groupName, resources)
	}

	// For other groups, write as directory-based template files
	return w.writeGroupDirectory(fs, groupName, resources)
}

// writeNamespaceFile writes the namespace as a single file in templates/
func (w *ChartWriter) writeNamespaceFile(fs machinery.Filesystem, namespace *unstructured.Unstructured) error {
	// Apply Helm templating
	yamlContent := w.convertToYAML(namespace)
	yamlContent = w.templater.ApplyHelmSubstitutions(yamlContent, namespace)

	// Write to templates/namespace.yaml
	filePath := filepath.Join(w.outputDir, "chart", "templates", "namespace.yaml")
	return w.writeFileWithNewline(fs, filePath, yamlContent)
}

// writeGroupDirectory writes resources as files in a group-specific directory
func (w *ChartWriter) writeGroupDirectory(
	fs machinery.Filesystem, groupName string,
	resources []*unstructured.Unstructured,
) error {
	var finalContent bytes.Buffer

	// Convert each resource to YAML and apply templating
	for i, resource := range resources {
		if i > 0 {
			finalContent.WriteString("---\n")
		}

		yamlContent := w.convertToYAML(resource)
		yamlContent = w.templater.ApplyHelmSubstitutions(yamlContent, resource)
		finalContent.WriteString(yamlContent)
	}

	// Write to templates/{groupName}/{groupName}.yaml
	dirPath := filepath.Join(w.outputDir, "chart", "templates", groupName)
	filePath := filepath.Join(dirPath, groupName+".yaml")

	return w.writeFileWithNewline(fs, filePath, finalContent.String())
}

// convertToYAML converts an unstructured object to YAML string with 2-space indentation
func (w *ChartWriter) convertToYAML(resource *unstructured.Unstructured) string {
	yamlBytes, err := yaml.Marshal(resource.Object)
	if err != nil {
		return fmt.Sprintf("# Error converting to YAML: %v\n", err)
	}
	return string(yamlBytes)
}

// shouldSplitFiles determines if resources in a group should be written as individual files
func (w *ChartWriter) shouldSplitFiles(groupName string) bool {
	return groupName == "crd" || groupName == "cert-manager" || groupName == "webhook" ||
		groupName == "prometheus" || groupName == "rbac" || groupName == "metrics" ||
		groupName == "extras"
}

// writeSplitFiles writes each resource in the group to its own file
func (w *ChartWriter) writeSplitFiles(
	fs machinery.Filesystem, groupName string,
	resources []*unstructured.Unstructured,
) error {
	// Create the group directory
	groupDir := filepath.Join(w.outputDir, "chart", "templates", groupName)
	if err := fs.FS.MkdirAll(groupDir, 0o755); err != nil {
		return fmt.Errorf("creating group directory %s: %w", groupDir, err)
	}

	// Write each resource to its own file
	for i, resource := range resources {
		fileName := w.generateFileName(resource, i)
		filePath := filepath.Join(groupDir, fileName)

		yamlContent := w.convertToYAML(resource)
		yamlContent = w.templater.ApplyHelmSubstitutions(yamlContent, resource)

		if err := w.writeFileWithNewline(fs, filePath, yamlContent); err != nil {
			return fmt.Errorf("writing resource file %s: %w", filePath, err)
		}
	}

	return nil
}

// generateFileName creates a unique filename for a resource based on its metadata
func (w *ChartWriter) generateFileName(resource *unstructured.Unstructured, index int) string {
	// Try to use the resource name if available
	if name := resource.GetName(); name != "" {
		// Remove project prefix from the filename for cleaner file names
		projectPrefix := w.templater.detectedPrefix + "-"
		fileName := name
		if after, ok := strings.CutPrefix(name, projectPrefix); ok {
			fileName = after
		}

		// Handle special cases where filename might be empty after prefix removal
		if fileName == "" {
			fileName = resource.GetKind()
			if fileName == "" {
				fileName = "resource"
			}
		}

		// For namespace-scoped RBAC (Role, RoleBinding), append namespace suffix
		// only for cross-namespace resources to prevent filename collisions.
		// Resources in the manager namespace don't need a suffix since they're unique.
		kind := resource.GetKind()
		namespace := resource.GetNamespace()
		if namespace != "" && (kind == "Role" || kind == "RoleBinding") {
			if namespace != w.managerNamespace {
				fileName = fmt.Sprintf("%s-%s", fileName, namespace)
			}
		}

		// Replace dots and other special characters with underscores for filename safety
		fileName = filepath.Base(fileName) // Remove any path separators
		return fmt.Sprintf("%s.yaml", fileName)
	}

	// Fall back to kind + index if no name
	kind := resource.GetKind()
	if kind == "" {
		kind = "resource"
	}
	return fmt.Sprintf("%s-%d.yaml", kind, index)
}

// writeFileWithNewline ensures the file ends with a newline
func (w *ChartWriter) writeFileWithNewline(fs machinery.Filesystem, filePath, content string) error {
	// Ensure content ends with newline
	if content != "" && content[len(content)-1] != '\n' {
		content += "\n"
	}

	// Create directory if it doesn't exist
	dir := filepath.Dir(filePath)
	if err := fs.FS.MkdirAll(dir, 0o755); err != nil {
		return fmt.Errorf("creating directory %s: %w", dir, err)
	}

	// Use afero to write directly through the filesystem
	if err := afero.WriteFile(fs.FS, filePath, []byte(content), 0o644); err != nil {
		return fmt.Errorf("writing file %s: %w", filePath, err)
	}
	return nil
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

const (
	kindNamespace          = "Namespace"
	kindCertificate        = "Certificate"
	kindService            = "Service"
	kindServiceAccount     = "ServiceAccount"
	kindRole               = "Role"
	kindClusterRole        = "ClusterRole"
	kindRoleBinding        = "RoleBinding"
	kindClusterRoleBinding = "ClusterRoleBinding"
	kindServiceMonitor     = "ServiceMonitor"
	kindIssuer             = "Issuer"
	kindValidatingWebhook  = "ValidatingWebhookConfiguration"
	kindMutatingWebhook    = "MutatingWebhookConfiguration"
	kindDeployment         = "Deployment"
	kindCRD                = "CustomResourceDefinition"

	// API versions
	apiVersionCertManager = "cert-manager.io/v1"
	apiVersionMonitoring  = "monitoring.coreos.com/v1"
)

// HelmTemplater handles converting YAML content to Helm templates
type HelmTemplater struct {
	detectedPrefix   string
	chartName        string
	managerNamespace string
}

// NewHelmTemplater creates a new Helm templater
func NewHelmTemplater(detectedPrefix, chartName, managerNamespace string) *HelmTemplater {
	return &HelmTemplater{
		detectedPrefix:   detectedPrefix,
		chartName:        chartName,
		managerNamespace: managerNamespace,
	}
}

// getDefaultContainerName extracts the container name from kubectl.kubernetes.io/default-container annotation.
// This allows the Helm plugin to work with any container name, not just "manager".
// If the annotation is not found, it falls back to "manager" for backward compatibility.
func (t *HelmTemplater) getDefaultContainerName(yamlContent string) string {
	// Look for kubectl.kubernetes.io/default-container annotation
	pattern := regexp.MustCompile(`kubectl\.kubernetes\.io/default-container:\s+(\S+)`)
	matches := pattern.FindStringSubmatch(yamlContent)
	if len(matches) > 1 {
		return matches[1]
	}
	// Fallback to "manager" for backward compatibility with older scaffolds
	return "manager"
}

// resourceNameTemplate creates a Helm template for a resource name with 63-char safety.
// Uses .resourceName helper which intelligently truncates when base + suffix > 63 chars.
// Template name is scoped to the chart to prevent collisions when used as a Helm dependency.
func (t *HelmTemplater) resourceNameTemplate(suffix string) string {
	return `{{ include "` + t.chartName + `.resourceName" (dict "suffix" "` + suffix + `" "context" $) }}`
}

// ApplyHelmSubstitutions converts YAML content to use Helm template syntax
func (t *HelmTemplater) ApplyHelmSubstitutions(yamlContent string, resource *unstructured.Unstructured) string {
	// Escape existing Go template syntax ({{ }}) FIRST before adding Helm templates.
	// Resources from install.yaml may contain templates that should be preserved as literal text.
	// For example: CRD default values, ConfigMap data, Secret URLs, annotations, etc.
	yamlContent = t.escapeExistingTemplateSyntax(yamlContent)

	// Apply conditional wrappers first
	yamlContent = t.addConditionalWrappers(yamlContent, resource)

	// Apply general project name substitutions
	yamlContent = t.substituteProjectNames(yamlContent, resource)

	// Apply namespace substitutions
	yamlContent = t.substituteNamespace(yamlContent, resource)

	// Apply cert-manager and webhook-specific templating AFTER other substitutions
	yamlContent = t.substituteCertManagerReferences(yamlContent, resource)

	yamlContent = t.substituteResourceNamesWithPrefix(yamlContent, resource)

	// Apply labels and annotations from Helm chart
	yamlContent = t.addHelmLabelsAndAnnotations(yamlContent, resource)

	// Apply resource-specific substitutions
	yamlContent = t.substituteRBACValues(yamlContent)

	// Apply deployment-specific templating
	if resource.GetKind() == kindDeployment {
		yamlContent = t.templateDeploymentFields(yamlContent)

		// Apply conditional logic for cert-manager related fields in deployments
		yamlContent = t.makeContainerArgsConditional(yamlContent)
		yamlContent = t.makeWebhookVolumeMountsConditional(yamlContent)
		yamlContent = t.makeWebhookVolumesConditional(yamlContent)
		yamlContent = t.makeMetricsVolumeMountsConditional(yamlContent)
		yamlContent = t.makeMetricsVolumesConditional(yamlContent)
	}

	// Apply port templating for Services and Deployments
	if resource.GetKind() == kindService || resource.GetKind() == kindDeployment {
		yamlContent = t.templatePorts(yamlContent, resource)
	}

	// Final tidy-up: avoid accidental blank lines after Helm if-block starts
	// Some replacements may introduce an empty line between a `{{- if ... }}`
	// and the following content; collapse that to ensure consistent formatting.
	yamlContent = t.collapseBlankLineAfterIf(yamlContent)

	return yamlContent
}

// escapeExistingTemplateSyntax escapes Go template syntax ({{ }}) in YAML to prevent
// Helm from parsing them. Converts existing templates to literal strings that Helm outputs as-is.
//
// Why this is needed:
// Resources from install.yaml may contain {{ }} in string fields that are NOT Helm templates.
// Without escaping, Helm will try to evaluate them and fail. For example:
//
//	CRD default: "Branch: {{ .Spec.Branch }}"  ->  ERROR: .Spec undefined
//
// How it works:
// Wraps non-Helm templates in string literals so Helm outputs them unchanged:
//
//	{{ .Field }}  ->  {{ "{{ .Field }}" }}
//
// When Helm renders this, it outputs the literal string: {{ .Field }}
//
// Smart detection:
// Only escapes templates that DON'T start with Helm keywords:
//   - .Release, .Values, .Chart (Helm built-ins)
//   - include, if, with, range, toYaml (Helm functions)
//
// This means our Helm templates work normally while existing templates are preserved.
func (t *HelmTemplater) escapeExistingTemplateSyntax(yamlContent string) string {
	// (?s) makes '.' match newlines so split-line templates produced by sigs.k8s.io/yaml's
	// ~80-column folding (e.g. "{{ .LongName\n    }}") are matched in a single pass.
	templatePattern := regexp.MustCompile(`(?s)\{\{(.*?)\}\}`)

	yamlContent = templatePattern.ReplaceAllStringFunc(yamlContent, func(match string) string {
		// Extract content between {{ and }}
		content := strings.TrimPrefix(match, "{{")
		content = strings.TrimSuffix(content, "}}")
		trimmedContent := strings.TrimSpace(content)

		// Check if this is a Helm template (starts with Helm keyword)
		helmPatterns := []string{
			"include ", "- include ",
			".Release.", "- .Release.",
			".Values.", "- .Values.",
			".Chart.", "- .Chart.",
			"toYaml ", "- toYaml ",
			"if ", "- if ",
			"end ", "- end ",
			"with ", "- with ",
			"range ", "- range ",
			"else", "- else",
		}

		// If it's a Helm template, keep it as-is
		for _, pattern := range helmPatterns {
			if strings.HasPrefix(trimmedContent, pattern) {
				return match
			}
		}

		// Otherwise, escape it to preserve as literal text
		// Collapse any newline+indent that sigs.k8s.io/yaml may have introduced via line-wrapping.
		collapsed := regexp.MustCompile(`\n[ \t]+`).ReplaceAllString(content, " ")

		// Before re-escaping for Go template string literals, unescape any YAML double-quoted
		// scalar escape sequences. yaml.Marshal emits \" for a literal " inside a double-quoted
		// YAML scalar; without this step the subsequent "→\" replacement double-escapes them to
		// \\" which breaks Helm's Go template parser: \\ becomes one backslash, then the next "
		// closes the string prematurely, leaving tokens like "asset-id" outside where "-" is a
		// bad character (U+002D).
		unescaped := strings.ReplaceAll(collapsed, `\"`, `"`)
		escapedContent := strings.ReplaceAll(unescaped, `"`, `\"`)

		// Wrap in Helm string literal: {{ "{{...}}" }}
		return `{{ "{{` + escapedContent + `}}" }}`
	})

	return yamlContent
}

// substituteProjectNames keeps original YAML as much as possible - only add Helm templating
func (t *HelmTemplater) substituteProjectNames(yamlContent string, _ *unstructured.Unstructured) string {
	return yamlContent
}

// substituteNamespace replaces manager namespace references with {{ .Release.Namespace }}
// while preserving cross-namespace references (e.g., infrastructure, production).
//
// DESIGN RATIONALE:
// We use regex-based replacement (not YAML parsing) because the content already contains
// Helm templates from previous substitutions, which would break YAML parsing.
//
// SAFETY GUARANTEES:
// 1. Namespace fields: Only replaces `namespace: ` (line-anchored regex)
// 2. DNS names: Only replaces `..` (dots on both sides prevent substring matches)
// 3. References: Only replaces `/` (word boundary prevents false matches)
//
// TESTED SCENARIOS:
// - All standard K8s resource types (ConfigMap, Secret, Ingress, etc.)
// - All monitoring resources (ServiceMonitor, PodMonitor)
// - All RBAC resources (Role, RoleBinding, with cross-namespace support)
// - All DNS patterns (.svc, .svc.cluster.local, .pod, .endpoints)
// - Custom CRDs with any structure
// - Cross-namespace preservation (infrastructure, production, etc.)
// - Substring bug prevention (namespace "user" doesn't break resource "users")
func (t *HelmTemplater) substituteNamespace(yamlContent string, resource *unstructured.Unstructured) string {
	managerNamespace := t.managerNamespace
	namespaceTemplate := "{{ .Release.Namespace }}"

	// 1. NAMESPACE FIELDS: Replace `namespace: `
	//    Pattern: Line-anchored to prevent false matches
	//    Example: `namespace: project-system` → `namespace: {{ .Release.Namespace }}`
	namespaceFieldPattern := regexp.MustCompile(`(?m)^(\s*)namespace:\s+` + regexp.QuoteMeta(managerNamespace) + `\s*$`)
	yamlContent = namespaceFieldPattern.ReplaceAllString(yamlContent, "${1}namespace: "+namespaceTemplate)

	// 2. RESOURCE REFERENCES: Replace `/resource-name`
	//    Pattern: Word boundary ensures we don't match partial words
	//    Example: `cert-manager.io/inject-ca-from: project-system/cert` → `{{ .Release.Namespace }}/cert`
	//    Example: `configMapRef: project-system/config` → `{{ .Release.Namespace }}/config`
	refPattern := regexp.MustCompile(`\b` + regexp.QuoteMeta(managerNamespace) + `/`)
	yamlContent = refPattern.ReplaceAllString(yamlContent, namespaceTemplate+"/")

	// 3. DNS NAMES: Replace `..` in Kubernetes DNS patterns
	//    Pattern: Dots on both sides ensure we only match DNS, not arbitrary strings
	//    Handles ALL K8s DNS patterns: .svc, .svc.cluster.local, .pod, .endpoints, etc.
	//    Example: `service.project-system.svc` → `service.{{ .Release.Namespace }}.svc`
	//    Example: `pod.project-system.pod.cluster.local` → `pod.{{ .Release.Namespace }}.pod.cluster.local`
	//
	//    SAFETY: This won't match:
	//    - Resource names: "users" (no dots around it)
	//    - Arbitrary strings: "my-application" (no dots)
	//    - Labels: "app=project-system" (no dots on both sides)
	dnsPattern := regexp.MustCompile(`\.` + regexp.QuoteMeta(managerNamespace) + `\.`)
	yamlContent = dnsPattern.ReplaceAllString(yamlContent, "."+namespaceTemplate+".")

	// 4. CERTIFICATE-SPECIFIC: Additional service name templating for cert-manager
	//    This is additive only and doesn't interfere with the above replacements
	if resource.GetKind() == kindCertificate {
		yamlContent = t.substituteCertificateDNSNames(yamlContent, resource)
	}

	return yamlContent
}

// substituteCertificateDNSNames replaces hardcoded DNS names in certificates with proper service templates
func (t *HelmTemplater) substituteCertificateDNSNames(yamlContent string, resource *unstructured.Unstructured) string {
	name := resource.GetName()

	// Replace service names with templated ones based on certificate type
	if strings.Contains(name, "metrics-cert") || strings.Contains(name, "metrics") {
		// Metrics certificates should point to metrics service
		// Use chart-specific resourceName helper for consistent naming with 63-char safety
		metricsServiceTemplate := "{{ include \"" + t.chartName + ".resourceName\" " +
			"(dict \"suffix\" \"controller-manager-metrics-service\" \"context\" $) }}"
		metricsServiceFQDN := metricsServiceTemplate + ".{{ include \"" + t.chartName + ".namespaceName\" $ }}.svc"
		metricsServiceFQDNCluster := metricsServiceTemplate +
			".{{ include \"" + t.chartName + ".namespaceName\" $ }}.svc.cluster.local"

		// Replace placeholders
		yamlContent = strings.ReplaceAll(yamlContent, "SERVICE_NAME.SERVICE_NAMESPACE.svc", metricsServiceFQDN)
		yamlContent = strings.ReplaceAll(yamlContent,
			"SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local", metricsServiceFQDNCluster)

		// Also replace hardcoded service names
		hardcodedMetricsService := t.detectedPrefix + "-controller-manager-metrics-service"
		yamlContent = strings.ReplaceAll(yamlContent, hardcodedMetricsService, metricsServiceTemplate)
	} else if strings.Contains(name, "serving-cert") || strings.Contains(name, "webhook") {
		hardcodedWebhookServiceShort := t.detectedPrefix + "-webhook-service"
		yamlContent = strings.ReplaceAll(yamlContent, hardcodedWebhookServiceShort, t.resourceNameTemplate("webhook-service"))
	}

	return yamlContent
}

// substituteCertManagerReferences applies cert-manager specific template substitutions
func (t *HelmTemplater) substituteCertManagerReferences(
	yamlContent string,
	resource *unstructured.Unstructured,
) string {
	kind := resource.GetKind()

	if kind == kindIssuer || kind == kindCertificate {
		hardcodedIssuerRef := t.detectedPrefix + "-selfsigned-issuer"
		yamlContent = strings.ReplaceAll(yamlContent, hardcodedIssuerRef, t.resourceNameTemplate("selfsigned-issuer"))
	}

	if kind == kindValidatingWebhook || kind == kindMutatingWebhook || kind == kindCRD {
		hardcodedService := "name: " + t.detectedPrefix + "-webhook-service"
		templatedService := "name: " + t.resourceNameTemplate("webhook-service")
		yamlContent = strings.ReplaceAll(yamlContent, hardcodedService, templatedService)
	}

	yamlContent = t.substituteCertManagerAnnotations(yamlContent)
	return yamlContent
}

// substituteResourceNamesWithPrefix templates ALL resource names using chart.serviceName helper.
// Generic regex-based approach works for any resource type without hardcoding specific names.
// Excludes container names since those are internal pod identifiers that don't need templating.
func (t *HelmTemplater) substituteResourceNamesWithPrefix(yamlContent string, _ *unstructured.Unstructured) string {
	namePattern := regexp.MustCompile(
		`(\s+)([a-zA-Z]*[Nn]ame):\s+` + regexp.QuoteMeta(t.detectedPrefix) + `(-[a-zA-Z0-9-]+)`)

	lines := strings.Split(yamlContent, "\n")
	result := make([]string, 0, len(lines))

	for i, line := range lines {
		if !namePattern.MatchString(line) {
			result = append(result, line)
			continue
		}

		// Check if this is a container name by looking at surrounding context
		// Container names appear after "containers:" and before other container fields (image, args, etc.)
		isContainerName := false
		if strings.Contains(line, "name:") {
			// Look backward for "containers:" within ~20 lines
			for j := i - 1; j >= 0 && j >= i-20; j-- {
				trimmed := strings.TrimSpace(lines[j])
				if trimmed == "containers:" {
					isContainerName = true
					break
				}
				// Stop if we hit a new top-level section
				if strings.HasPrefix(lines[j], "  ") && strings.HasSuffix(trimmed, ":") &&
					(trimmed == "spec:" || trimmed == "template:" || trimmed == "volumes:") {
					break
				}
			}
		}

		if isContainerName {
			// Don't template container names - keep them as-is
			result = append(result, line)
		} else {
			// Template other resource names
			templatedLine := namePattern.ReplaceAllStringFunc(line, func(match string) string {
				parts := namePattern.FindStringSubmatch(match)
				if len(parts) < 4 {
					return match
				}

				indent := parts[1]
				fieldName := parts[2]
				suffix := parts[3][1:] // Remove leading dash

				return indent + fieldName + ": " + t.resourceNameTemplate(suffix)
			})
			result = append(result, templatedLine)
		}
	}

	return strings.Join(result, "\n")
}

// addHelmLabelsAndAnnotations replaces kustomize managed-by labels with Helm equivalents
func (t *HelmTemplater) addHelmLabelsAndAnnotations(
	yamlContent string,
	resource *unstructured.Unstructured,
) string {
	// Replace app.kubernetes.io/managed-by: kustomize with Helm template
	// Use regex to handle different whitespace patterns
	managedByRegex := regexp.MustCompile(`(\s*)app\.kubernetes\.io/managed-by:\s+kustomize`)
	yamlContent = managedByRegex.ReplaceAllString(yamlContent, "${1}app.kubernetes.io/managed-by: {{ .Release.Service }}")

	hardcodedNameLabel := "app.kubernetes.io/name: " + t.detectedPrefix
	templatedNameLabel := "app.kubernetes.io/name: {{ include \"" + t.chartName + ".name\" . }}"
	yamlContent = strings.ReplaceAll(yamlContent, hardcodedNameLabel, templatedNameLabel)

	// Add standard Helm labels to metadata.labels and selectors
	yamlContent = t.addStandardHelmLabels(yamlContent, resource)

	return yamlContent
}

// checkExistingLabels checks if standard Helm labels already exist in a labels section
// by looking both backward and forward from the current position
func checkExistingLabels(lines []string, currentIndex int, indent string) (hasChart, hasInstance, hasManagedBy bool) {
	// Look backward from current position (managed-by often appears before name in kustomize output)
	for j := currentIndex - 1; j >= 0 && j >= currentIndex-10; j-- {
		backLine := lines[j]
		backTrimmed := strings.TrimSpace(backLine)
		backIndent, _ := leadingWhitespace(backLine)

		// Stop if we've moved out of the labels section
		if backTrimmed == "labels:" {
			break
		}
		if backTrimmed != "" && len(backIndent) < len(indent) {
			break
		}

		if strings.Contains(backLine, "helm.sh/chart:") {
			hasChart = true
		}
		if strings.Contains(backLine, "app.kubernetes.io/instance:") {
			hasInstance = true
		}
		if strings.Contains(backLine, "app.kubernetes.io/managed-by:") {
			hasManagedBy = true
		}
	}

	// Look ahead from current position
	for j := currentIndex + 1; j < len(lines) && j < currentIndex+10; j++ {
		nextLine := lines[j]
		nextTrimmed := strings.TrimSpace(nextLine)
		nextIndent, _ := leadingWhitespace(nextLine)

		// Stop if we've moved to a new section
		if nextTrimmed != "" && len(nextIndent) < len(indent) {
			break
		}

		if strings.Contains(nextLine, "helm.sh/chart:") {
			hasChart = true
		}
		if strings.Contains(nextLine, "app.kubernetes.io/instance:") {
			hasInstance = true
		}
		if strings.Contains(nextLine, "app.kubernetes.io/managed-by:") {
			hasManagedBy = true
		}
	}

	return hasChart, hasInstance, hasManagedBy
}

// addStandardHelmLabels adds standard Helm labels (helm.sh/chart, app.kubernetes.io/instance,
// and app.kubernetes.io/managed-by) to all labels sections except selectors (which must be immutable)
func (t *HelmTemplater) addStandardHelmLabels(yamlContent string, _ *unstructured.Unstructured) string {
	lines := strings.Split(yamlContent, "\n")
	result := make([]string, 0, len(lines)+10) // Pre-allocate with extra space for added labels
	inSelector := false

	for i := range lines {
		line := lines[i]
		result = append(result, line)

		// Track if we're in a selector section (matchLabels or spec.selector for Services)
		trimmed := strings.TrimSpace(line)
		isMatchLabels := trimmed == "matchLabels:"
		isSelectorWithoutMatchLabels := trimmed == "selector:" && i+1 < len(lines) &&
			!strings.Contains(lines[i+1], "matchLabels")
		if isMatchLabels || isSelectorWithoutMatchLabels {
			inSelector = true
		}

		// Exit selector section when we hit a line with less indentation
		if inSelector && trimmed != "" && !strings.HasPrefix(trimmed, "app.kubernetes.io/") &&
			!strings.HasPrefix(trimmed, "control-plane:") && strings.Contains(trimmed, ":") {
			inSelector = false
		}

		// Add standard Helm labels to any labels section (metadata.labels, template.metadata.labels)
		// but NOT to selectors (which must remain immutable)
		if !inSelector && strings.Contains(line, "app.kubernetes.io/name:") {
			indent, _ := leadingWhitespace(line)

			// Check if we're in a labels section by looking backwards
			isInLabelsSection := false
			for j := i - 1; j >= 0 && j >= i-5; j-- {
				if strings.TrimSpace(lines[j]) == "labels:" {
					isInLabelsSection = true
					break
				}
				if strings.TrimSpace(lines[j]) == "metadata:" {
					break
				}
			}

			if !isInLabelsSection {
				continue
			}

			// Check if standard labels already exist in this labels section
			hasHelmChart, hasInstance, hasManagedBy := checkExistingLabels(lines, i, indent)

			// Add helm.sh/chart if it doesn't exist
			if !hasHelmChart {
				result = append(result, indent+"helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace \"+\" \"_\" }}")
			}

			// Add app.kubernetes.io/instance if it doesn't exist
			if !hasInstance {
				result = append(result, indent+"app.kubernetes.io/instance: {{ .Release.Name }}")
			}

			// Add app.kubernetes.io/managed-by if it doesn't exist (per Helm best practices)
			if !hasManagedBy {
				result = append(result, indent+"app.kubernetes.io/managed-by: {{ .Release.Service }}")
			}
		}
	}

	return strings.Join(result, "\n")
}

// substituteRBACValues applies RBAC-specific template substitutions
func (t *HelmTemplater) substituteRBACValues(yamlContent string) string {
	roleRefBlockPattern := regexp.MustCompile(
		`(?s)(roleRef:\s*\n(?:\s+\w+:.*\n)*?)(\s+)name:\s+` +
			regexp.QuoteMeta(t.detectedPrefix) + `-manager-role`)
	yamlContent = roleRefBlockPattern.ReplaceAllString(
		yamlContent, `${1}${2}name: `+t.resourceNameTemplate("manager-role"))

	roleRefBlockPatternSimple := regexp.MustCompile(
		`(?s)(roleRef:\s*\n(?:\s+\w+:.*\n)*?)(\s+)name:\s+manager-role`)
	yamlContent = roleRefBlockPatternSimple.ReplaceAllString(
		yamlContent, `${1}${2}name: `+t.resourceNameTemplate("manager-role"))

	return yamlContent
}

// substituteCertManagerAnnotations replaces hardcoded certificate references in annotations
func (t *HelmTemplater) substituteCertManagerAnnotations(yamlContent string) string {
	hardcodedServingCert := t.detectedPrefix + "-serving-cert"
	yamlContent = strings.ReplaceAll(yamlContent, hardcodedServingCert, t.resourceNameTemplate("serving-cert"))

	hardcodedMetricsCert := t.detectedPrefix + "-metrics-certs"
	yamlContent = strings.ReplaceAll(yamlContent, hardcodedMetricsCert, t.resourceNameTemplate("metrics-certs"))

	return yamlContent
}

// templateDeploymentFields converts deployment-specific fields to Helm templates
func (t *HelmTemplater) templateDeploymentFields(yamlContent string) string {
	// Template replicas from values.yaml (manager.replicas)
	yamlContent = t.templateReplicas(yamlContent)
	// Template configuration fields
	yamlContent = t.templateImageReference(yamlContent)
	yamlContent = t.templateEnvironmentVariables(yamlContent)
	yamlContent = t.templateImagePullSecrets(yamlContent)
	yamlContent = t.templatePodSecurityContext(yamlContent)
	yamlContent = t.templateContainerSecurityContext(yamlContent)
	yamlContent = t.templateResources(yamlContent)
	yamlContent = t.templateSecurityContexts(yamlContent)
	yamlContent = t.templateVolumeMounts(yamlContent)
	yamlContent = t.templateVolumes(yamlContent)
	yamlContent = t.templateControllerManagerArgs(yamlContent)
	yamlContent = t.templateBasicWithStatement(
		yamlContent,
		"nodeSelector",
		"spec.template.spec",
		".Values.manager.nodeSelector",
	)
	yamlContent = t.templateBasicWithStatement(
		yamlContent,
		"affinity",
		"spec.template.spec",
		".Values.manager.affinity",
	)
	yamlContent = t.templateBasicWithStatement(
		yamlContent,
		"tolerations",
		"spec.template.spec",
		".Values.manager.tolerations",
	)

	return yamlContent
}

// templateReplicas replaces deployment spec.replicas with .Values.manager.replicas so the
// value in values.yaml is used. With leader election, only one replica is active at a time;
// multiple replicas are valid for HA (standby replicas).
func (t *HelmTemplater) templateReplicas(yamlContent string) string {
	if strings.Contains(yamlContent, ".Values.manager.replicas") {
		return yamlContent
	}
	// Match a line that is exactly "  replicas: " (deployment spec.replicas).
	// Preserve indentation so the replacement fits the existing YAML structure.
	replicasPattern := regexp.MustCompile(`(?m)^(\s*)replicas:\s*\d+\s*$`)
	return replicasPattern.ReplaceAllString(yamlContent, "${1}replicas: {{ .Values.manager.replicas }}")
}

// templateEnvironmentVariables exposes environment variables via values.yaml
func (t *HelmTemplater) templateEnvironmentVariables(yamlContent string) string {
	containerName := t.getDefaultContainerName(yamlContent)
	// Check for both literal container name and templated container name
	hasLiteralName := strings.Contains(yamlContent, "name: "+containerName)
	hasTemplatedName := strings.Contains(yamlContent, `name: {{ include "`) && strings.Contains(yamlContent, `"manager"`)
	if !hasLiteralName && !hasTemplatedName {
		return yamlContent
	}

	lines := strings.Split(yamlContent, "\n")
	for i := range lines {
		if strings.TrimSpace(lines[i]) != "env:" {
			continue
		}

		indentStr, indentLen := leadingWhitespace(lines[i])
		end := i + 1
		for ; end < len(lines); end++ {
			trimmed := strings.TrimSpace(lines[end])
			if trimmed == "" {
				break
			}
			lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
			if lineIndent < indentLen {
				break
			}
			if lineIndent == indentLen && !strings.HasPrefix(trimmed, "-") {
				break
			}
		}

		nextLine := ""
		if i+1 < len(lines) {
			nextLine = lines[i+1]
		}
		if strings.Contains(nextLine, ".Values.manager.env") || strings.Contains(nextLine, "envOverrides") {
			return yamlContent
		}

		childIndent := indentStr + "  "
		childIndentWidth := strconv.Itoa(len(childIndent))
		// Env list + envOverrides (CLI --set). Secret refs go in env list.
		hasEnv := `{{- if or .Values.manager.env (and (kindIs "map" .Values.manager.envOverrides) ` +
			`(not (empty .Values.manager.envOverrides))) }}`
		block := make([]string, 0, 22)
		block = append(block,
			indentStr+"env:",
			hasEnv,
			childIndent+`{{- if .Values.manager.env }}`,
			childIndent+"{{- toYaml .Values.manager.env | nindent "+childIndentWidth+" }}",
			childIndent+`{{- end }}`,
			childIndent+`{{- if kindIs "map" .Values.manager.envOverrides }}`,
			childIndent+`{{- range $k, $v := .Values.manager.envOverrides }}`,
			childIndent+`- name: {{ $k }}`,
			childIndent+`  value: {{ $v | quote }}`,
			childIndent+`{{ end }}`,
			childIndent+`{{- end }}`,
			childIndent+`{{- else }}`,
			childIndent+"[]",
			childIndent+`{{- end }}`,
		)

		newLines := append([]string{}, lines[:i]...)
		newLines = append(newLines, block...)
		newLines = append(newLines, lines[end:]...)
		return strings.Join(newLines, "\n")
	}

	return yamlContent
}

// templateResources converts resource sections to Helm templates
func (t *HelmTemplater) templateResources(yamlContent string) string {
	containerName := t.getDefaultContainerName(yamlContent)
	// Check for both literal container name and templated container name
	hasLiteralName := strings.Contains(yamlContent, "name: "+containerName)
	hasTemplatedName := strings.Contains(yamlContent, `name: {{ include "`) && strings.Contains(yamlContent, `"manager"`)
	if (!hasLiteralName && !hasTemplatedName) || !strings.Contains(yamlContent, "resources:") {
		return yamlContent
	}

	lines := strings.Split(yamlContent, "\n")
	for i := range lines {
		if strings.TrimSpace(lines[i]) != "resources:" {
			continue
		}

		indentStr, indentLen := leadingWhitespace(lines[i])
		end := i + 1
		for ; end < len(lines); end++ {
			trimmed := strings.TrimSpace(lines[end])
			if trimmed == "" {
				break
			}
			lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
			if lineIndent < indentLen {
				break
			}
			// stop at same-level keys that are not part of the resources mapping
			if lineIndent == indentLen && !strings.Contains(trimmed, ":") {
				break
			}
			if lineIndent == indentLen && strings.HasSuffix(trimmed, ":") {
				break
			}
		}

		if i+1 < len(lines) && strings.Contains(lines[i+1], ".Values.manager.resources") {
			return yamlContent
		}

		childIndent := indentStr + "  "
		childIndentWidth := strconv.Itoa(len(childIndent))

		block := []string{
			indentStr + "resources:",
			childIndent + "{{- if .Values.manager.resources }}",
			childIndent + "{{- toYaml .Values.manager.resources | nindent " + childIndentWidth + " }}",
			childIndent + "{{- else }}",
			childIndent + "{}",
			childIndent + "{{- end }}",
		}

		newLines := append([]string{}, lines[:i]...)
		newLines = append(newLines, block...)
		newLines = append(newLines, lines[end:]...)
		return strings.Join(newLines, "\n")
	}

	return yamlContent
}

// templateSecurityContexts preserves security contexts from kustomize output
func (t *HelmTemplater) templateSecurityContexts(yamlContent string) string {
	// Security contexts are preserved as-is from the kustomize output to maintain
	// the exact security configuration without interfering with other container fields
	return yamlContent
}

// templateVolumeMounts converts volumeMounts sections to keep them as-is since they're webhook-specific
func (t *HelmTemplater) templateVolumeMounts(yamlContent string) string {
	// For webhook volumeMounts, we keep them as-is since they're required for webhook functionality
	// They will be conditionally included based on webhook configuration
	return yamlContent
}

// templateVolumes converts volumes sections to keep them as-is since they're webhook-specific
func (t *HelmTemplater) templateVolumes(yamlContent string) string {
	// For webhook volumes, we keep them as-is since they're required for webhook functionality
	// They will be conditionally included based on webhook configuration
	return yamlContent
}

// templateImagePullSecrets exposes imagePullSecrets via values.yaml
func (t *HelmTemplater) templateImagePullSecrets(yamlContent string) string {
	if !strings.Contains(yamlContent, "imagePullSecrets:") {
		return yamlContent
	}

	lines := strings.Split(yamlContent, "\n")
	for i := range lines {
		// Use prefix to allow `imagePullSecrets: []` to be preserved
		if !strings.HasPrefix(strings.TrimSpace(lines[i]), "imagePullSecrets:") {
			continue
		}
		indentStr, indentLen := leadingWhitespace(lines[i])
		end := i + 1
		for ; end < len(lines); end++ {
			trimmed := strings.TrimSpace(lines[end])
			if trimmed == "" {
				break
			}
			lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
			if lineIndent < indentLen {
				break
			}
			if lineIndent == indentLen && !strings.HasPrefix(trimmed, "-") {
				break
			}
		}

		if i+1 < len(lines) && strings.Contains(lines[i+1], ".Values.manager.imagePullSecrets") {
			return yamlContent
		}

		childIndent := indentStr + "  "
		childIndentWidth := strconv.Itoa(len(childIndent))

		block := []string{
			indentStr + "{{- if .Values.manager.imagePullSecrets }}",
			indentStr + "imagePullSecrets:",
			childIndent + "{{- toYaml .Values.manager.imagePullSecrets | nindent " + childIndentWidth + " }}",
			indentStr + "{{- end }}",
		}

		newLines := append([]string{}, lines[:i]...)
		newLines = append(newLines, block...)
		newLines = append(newLines, lines[end:]...)
		return strings.Join(newLines, "\n")
	}

	return yamlContent
}

// templatePodSecurityContext exposes podSecurityContext via values.yaml
func (t *HelmTemplater) templatePodSecurityContext(yamlContent string) string {
	if !strings.Contains(yamlContent, "securityContext:") {
		return yamlContent
	}

	lines := strings.Split(yamlContent, "\n")
	for i := range lines {
		if strings.TrimSpace(lines[i]) != "securityContext:" {
			continue
		}

		indentStr, indentLen := leadingWhitespace(lines[i])
		end := i + 1
		for ; end < len(lines); end++ {
			trimmed := strings.TrimSpace(lines[end])
			if trimmed == "" {
				break
			}
			lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
			if lineIndent <= indentLen {
				break
			}
		}

		if end >= len(lines) {
			break
		}

		if !strings.HasPrefix(strings.TrimSpace(lines[end]), "serviceAccountName:") {
			continue
		}

		if i+1 < len(lines) && strings.Contains(lines[i+1], ".Values.manager.podSecurityContext") {
			return yamlContent
		}

		childIndent := indentStr + "  "
		childIndentWidth := strconv.Itoa(len(childIndent))

		block := []string{
			indentStr + "securityContext:",
			childIndent + "{{- if .Values.manager.podSecurityContext }}",
			childIndent + "{{- toYaml .Values.manager.podSecurityContext | nindent " + childIndentWidth + " }}",
			childIndent + "{{- else }}",
			childIndent + "{}",
			childIndent + "{{- end }}",
		}

		newLines := append([]string{}, lines[:i]...)
		newLines = append(newLines, block...)
		newLines = append(newLines, lines[end:]...)
		return strings.Join(newLines, "\n")
	}

	return yamlContent
}

// templateContainerSecurityContext exposes container securityContext via values.yaml
func (t *HelmTemplater) templateContainerSecurityContext(yamlContent string) string {
	containerName := t.getDefaultContainerName(yamlContent)
	// Check for both literal container name and templated container name
	hasLiteralName := strings.Contains(yamlContent, "name: "+containerName)
	hasTemplatedName := strings.Contains(yamlContent, `name: {{ include "`) && strings.Contains(yamlContent, `"manager"`)
	if (!hasLiteralName && !hasTemplatedName) || !strings.Contains(yamlContent, "securityContext:") {
		return yamlContent
	}

	lines := strings.Split(yamlContent, "\n")
	for i := range lines {
		if strings.TrimSpace(lines[i]) != "securityContext:" {
			continue
		}

		indentStr, indentLen := leadingWhitespace(lines[i])
		end := i + 1
		for ; end < len(lines); end++ {
			trimmed := strings.TrimSpace(lines[end])
			if trimmed == "" {
				break
			}
			lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
			if lineIndent <= indentLen {
				break
			}
		}

		if end >= len(lines) {
			break
		}

		if strings.HasPrefix(strings.TrimSpace(lines[end]), "serviceAccountName:") {
			continue
		}

		lookAheadEnd := min(end+5, len(lines))
		joined := strings.Join(lines[i:lookAheadEnd], "\n")
		if strings.Contains(joined, ".Values.manager.securityContext") {
			return yamlContent
		}

		childIndent := indentStr + "  "
		childIndentWidth := strconv.Itoa(len(childIndent))

		block := []string{
			indentStr + "securityContext:",
			childIndent + "{{- if .Values.manager.securityContext }}",
			childIndent + "{{- toYaml .Values.manager.securityContext | nindent " + childIndentWidth + " }}",
			childIndent + "{{- else }}",
			childIndent + "{}",
			childIndent + "{{- end }}",
		}

		newLines := append([]string{}, lines[:i]...)
		newLines = append(newLines, block...)
		newLines = append(newLines, lines[end:]...)
		return strings.Join(newLines, "\n")
	}

	return yamlContent
}

func leadingWhitespace(line string) (string, int) {
	trimmed := strings.TrimLeft(line, " \t")
	indentLen := len(line) - len(trimmed)
	return line[:indentLen], indentLen
}

// templateControllerManagerArgs exposes controller manager args via values.yaml while keeping core defaults
func (t *HelmTemplater) templateControllerManagerArgs(yamlContent string) string {
	containerName := t.getDefaultContainerName(yamlContent)
	// Check for both literal container name and templated container name
	hasLiteralName := strings.Contains(yamlContent, "name: "+containerName)
	hasTemplatedName := strings.Contains(yamlContent, `name: {{ include "`) && strings.Contains(yamlContent, `"manager"`)
	if !hasLiteralName && !hasTemplatedName {
		return yamlContent
	}

	argsPattern := regexp.MustCompile(`(?m)([ \t]+)args:\n((?:[ \t]+-.*\n)+)`)
	loc := argsPattern.FindStringSubmatchIndex(yamlContent)
	if loc == nil {
		return yamlContent
	}

	match := yamlContent[loc[0]:loc[1]]
	if strings.Contains(match, ".Values.manager.args") {
		return yamlContent
	}

	indent := yamlContent[loc[2]:loc[3]]
	itemsBlock := yamlContent[loc[4]:loc[5]]

	itemIndent := indent + "  "
	lines := strings.Split(itemsBlock, "\n")
	var (
		metricsLine    string
		metricsIndent  string
		healthLine     string
		preservedLines []string
	)

	for _, rawLine := range lines {
		line := strings.TrimRight(rawLine, "\r")
		trimmed := strings.TrimSpace(line)
		if trimmed == "" {
			continue
		}

		if itemIndent == indent+"  " {
			if idx := strings.Index(line, "-"); idx > 0 {
				itemIndent = line[:idx]
			}
		}

		switch {
		case strings.Contains(trimmed, "--metrics-bind-address"):
			metricsLine = line
			if idx := strings.Index(line, "-"); idx > 0 {
				metricsIndent = line[:idx]
			}
		case strings.Contains(trimmed, "--health-probe-bind-address"):
			healthLine = line
		case strings.Contains(trimmed, "--webhook-cert-path"),
			strings.Contains(trimmed, "--metrics-cert-path"):
			preservedLines = append(preservedLines, line)
		default:
			// Remaining args will be handled through values.yaml
		}
	}

	var builder strings.Builder
	builder.WriteString(indent)
	builder.WriteString("args:\n")

	if metricsLine != "" {
		if metricsIndent == "" {
			metricsIndent = itemIndent
		}
		builder.WriteString(metricsIndent)
		builder.WriteString("{{- if .Values.metrics.enable }}\n")
		builder.WriteString(metricsLine)
		builder.WriteString("\n")
		builder.WriteString(metricsIndent)
		builder.WriteString("{{- else }}\n")
		builder.WriteString(metricsIndent)
		builder.WriteString("# Bind to :0 to disable the controller-runtime managed metrics server\n")
		builder.WriteString(metricsIndent)
		builder.WriteString("- --metrics-bind-address=0\n")
		builder.WriteString(metricsIndent)
		builder.WriteString("{{- end }}\n")
	}
	if healthLine != "" {
		builder.WriteString(healthLine)
		builder.WriteString("\n")
	}

	builder.WriteString(itemIndent)
	builder.WriteString("{{- range .Values.manager.args }}\n")
	builder.WriteString(itemIndent)
	builder.WriteString("- {{ . }}\n")
	builder.WriteString(itemIndent)
	builder.WriteString("{{- end }}\n")

	for _, line := range preservedLines {
		builder.WriteString(line)
		builder.WriteString("\n")
	}

	newBlock := strings.TrimRight(builder.String(), "\n") + "\n"

	return yamlContent[:loc[0]] + newBlock + yamlContent[loc[1]:]
}

// templateImageReference converts hardcoded image references to Helm templates
func (t *HelmTemplater) templateImageReference(yamlContent string) string {
	containerName := t.getDefaultContainerName(yamlContent)
	// Check for both literal container name and templated container name (which may have been
	// converted by substituteResourceNamesWithPrefix before this function runs)
	hasLiteralName := strings.Contains(yamlContent, "name: "+containerName)
	hasTemplatedName := strings.Contains(yamlContent, `name: {{ include "`) && strings.Contains(yamlContent, `"manager"`)
	if !hasLiteralName && !hasTemplatedName {
		return yamlContent
	}

	lines := strings.Split(yamlContent, "\n")
	for i := 0; i < len(lines); i++ {
		trimmed := strings.TrimSpace(lines[i])
		if !strings.HasPrefix(trimmed, "image:") {
			continue
		}

		if strings.Contains(lines[i], ".Values.manager.image.repository") {
			return yamlContent
		}

		indentStr, indentLen := leadingWhitespace(lines[i])

		end := i + 1
		for ; end < len(lines); end++ {
			nextTrimmed := strings.TrimSpace(lines[end])
			if nextTrimmed == "" {
				break
			}
			lineIndent := len(lines[end]) - len(strings.TrimLeft(lines[end], " \t"))
			if lineIndent <= indentLen {
				break
			}
			// Stop when we reach a sibling key like env:, args:, etc.
			if lineIndent == indentLen+2 && strings.HasSuffix(nextTrimmed, ":") {
				if strings.Contains(nextTrimmed, "imagePullPolicy") {
					continue
				}
				break
			}
		}

		// Remove any existing imagePullPolicy line inside the block
		blockLines := lines[i+1 : end]
		filtered := make([]string, 0, len(blockLines))
		for _, line := range blockLines {
			if strings.Contains(strings.TrimSpace(line), "imagePullPolicy") {
				continue
			}
			filtered = append(filtered, line)
		}
		lines = append(lines[:i+1], append(filtered, lines[end:]...)...)
		end = i + 1 + len(filtered)

		imageLine := indentStr + "image: \"{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}\""
		pullPolicyLine := indentStr + "imagePullPolicy: {{ .Values.manager.image.pullPolicy }}"

		remainder := lines[end:]
		if len(remainder) > 0 && strings.HasPrefix(strings.TrimSpace(remainder[0]), "imagePullPolicy:") {
			remainder = remainder[1:]
		}

		newLines := append([]string{}, lines[:i]...)
		newLines = append(newLines, imageLine, pullPolicyLine)
		newLines = append(newLines, remainder...)
		return strings.Join(newLines, "\n")
	}

	return yamlContent
}

func (t *HelmTemplater) templateBasicWithStatement(
	yamlContent string,
	key string,
	parentKey string,
	valuePath string,
) string {
	lines := strings.Split(yamlContent, "\n")
	yamlKey := fmt.Sprintf("%s:", key)

	var start, end int
	var indentLen int
	if !strings.Contains(yamlContent, yamlKey) {
		// Find parent block start if the key is missing
		pKeyParts := strings.Split(parentKey, ".")
		pKeyIdx := 0
		pKeyInit := false
		currIndent := 0
		for i := range len(lines) {
			_, lineIndent := leadingWhitespace(lines[i])
			if pKeyInit && lineIndent <= currIndent {
				return yamlContent
			}
			if !strings.HasPrefix(strings.TrimSpace(lines[i]), pKeyParts[pKeyIdx]) {
				continue
			}

			// Parent key part found
			pKeyIdx++
			pKeyInit = true
			if pKeyIdx >= len(pKeyParts) {
				start = i + 1
				end = start
				break
			}
		}
		_, indentLen = leadingWhitespace(lines[start])
	} else {
		// Find the existing block
		for i := range len(lines) {
			if !strings.HasPrefix(strings.TrimSpace(lines[i]), key) {
				continue
			}
			start = i
			end = i + 1
			trimmed := strings.TrimSpace(lines[i])
			if len(trimmed) == len(yamlKey) {
				_, indentLenSearch := leadingWhitespace(lines[i])
				for j := end; j < len(lines); j++ {
					_, indentLenLine := leadingWhitespace(lines[j])
					if indentLenLine <= indentLenSearch {
						end = j
						break
					}
				}
			}
		}
		_, indentLen = leadingWhitespace(lines[start])
	}

	indentStr := strings.Repeat(" ", indentLen)

	var builder strings.Builder
	builder.WriteString(indentStr)
	builder.WriteString("{{- with ")
	builder.WriteString(valuePath)
	builder.WriteString(" }}\n")
	builder.WriteString(indentStr)
	builder.WriteString(yamlKey)
	builder.WriteString(" {{ toYaml . | nindent ")
	builder.WriteString(strconv.Itoa(indentLen + 4))
	builder.WriteString(" }}\n")
	builder.WriteString(indentStr)
	builder.WriteString("{{- end }}\n")

	newBlock := strings.TrimRight(builder.String(), "\n")

	newLines := append([]string{}, lines[:start]...)
	newLines = append(newLines, strings.Split(newBlock, "\n")...)
	newLines = append(newLines, lines[end:]...)
	return strings.Join(newLines, "\n")
}

// makeWebhookAnnotationsConditional makes only cert-manager annotations conditional, not the entire webhook
func (t *HelmTemplater) makeWebhookAnnotationsConditional(yamlContent string) string {
	// Find cert-manager.io/inject-ca-from annotation and make it conditional
	if strings.Contains(yamlContent, "cert-manager.io/inject-ca-from") {
		// Replace the cert-manager annotation with conditional wrapper
		certManagerPattern := regexp.MustCompile(`(\s+)cert-manager\.io/inject-ca-from:\s*[^\n]+`)
		yamlContent = certManagerPattern.ReplaceAllStringFunc(yamlContent, func(match string) string {
			// Extract the indentation
			indentMatch := regexp.MustCompile(`^(\s+)`).FindStringSubmatch(match)
			indent := ""
			if len(indentMatch) > 1 {
				indent = indentMatch[1]
			}

			// Extract the annotation line with proper indentation
			annotationLine := strings.TrimSpace(match)

			return fmt.Sprintf("%s{{- if .Values.certManager.enable }}\n%s%s\n%s{{- end }}",
				indent, indent, annotationLine, indent)
		})
	}

	return yamlContent
}

// makeContainerArgsConditional makes webhook-cert-path and metrics-cert-path args conditional on certManager.enable
func (t *HelmTemplater) makeContainerArgsConditional(yamlContent string) string {
	// Make webhook-cert-path arg conditional on certManager.enable
	if strings.Contains(yamlContent, "--webhook-cert-path") {
		// Match only spaces/tabs for indent to avoid consuming the newline
		webhookArgPattern := regexp.MustCompile(`([ \t]+)-\s*--webhook-cert-path=[^\n]*`)
		yamlContent = webhookArgPattern.ReplaceAllStringFunc(yamlContent, func(match string) string {
			indentMatch := regexp.MustCompile(`^(\s+)`).FindStringSubmatch(match)
			indent := ""
			if len(indentMatch) > 1 {
				indent = indentMatch[1]
			}

			argLine := strings.TrimSpace(match)
			return fmt.Sprintf("%s{{- if .Values.certManager.enable }}\n%s%s\n%s{{- end }}",
				indent, indent, argLine, indent)
		})
	}

	// Make metrics-cert-path arg conditional on certManager.enable AND metrics.enable
	if strings.Contains(yamlContent, "--metrics-cert-path") {
		// Match only spaces/tabs for indent to avoid consuming the newline
		metricsArgPattern := regexp.MustCompile(`([ \t]+)-\s*--metrics-cert-path=[^\n]*`)
		yamlContent = metricsArgPattern.ReplaceAllStringFunc(yamlContent, func(match string) string {
			indentMatch := regexp.MustCompile(`^(\s+)`).FindStringSubmatch(match)
			indent := ""
			if len(indentMatch) > 1 {
				indent = indentMatch[1]
			}

			argLine := strings.TrimSpace(match)
			return fmt.Sprintf("%s{{- if and .Values.certManager.enable .Values.metrics.enable }}\n%s%s\n%s{{- end }}",
				indent, indent, argLine, indent)
		})
	}

	return yamlContent
}

func makeYamlContent(match string) string {
	lines := strings.Split(match, "\n")
	if len(lines) > 0 {
		indent := ""
		if len(lines[0]) > 0 && lines[0][0] == ' ' {
			// Count leading spaces
			for _, char := range lines[0] {
				if char == ' ' {
					indent += " "
				} else {
					break
				}
			}
		}

		// Reconstruct the block with conditional wrapper
		var result strings.Builder
		result.WriteString(fmt.Sprintf("%s{{- if .Values.certManager.enable }}\n", indent))
		for _, line := range lines {
			result.WriteString(line + "\n")
		}
		result.WriteString(fmt.Sprintf("%s{{- end }}", indent))
		return result.String()
	}
	return match
}

// makeWebhookVolumesConditional makes webhook volumes conditional on certManager.enable
func (t *HelmTemplater) makeWebhookVolumesConditional(yamlContent string) string {
	// Make webhook volumes conditional on certManager.enable
	if strings.Contains(yamlContent, "webhook-certs") && strings.Contains(yamlContent, "secretName: webhook-server-cert") {
		// Match only spaces/tabs for indent to avoid consuming the newline
		volumePattern := regexp.MustCompile(`([ \t]+)-\s*name:\s*webhook-certs[\s\S]*?secretName:\s*webhook-server-cert`)
		yamlContent = volumePattern.ReplaceAllStringFunc(yamlContent, makeYamlContent)
	}

	return yamlContent
}

// makeWebhookVolumeMountsConditional makes webhook volumeMounts conditional on certManager.enable
func (t *HelmTemplater) makeWebhookVolumeMountsConditional(yamlContent string) string {
	// Make webhook volumeMounts conditional on certManager.enable
	webhookCertsPath := "/tmp/k8s-webhook-server/serving-certs"
	if strings.Contains(yamlContent, "webhook-certs") && strings.Contains(yamlContent, webhookCertsPath) {
		// Match only spaces/tabs for indent to avoid consuming the newline
		mountPattern := regexp.MustCompile(
			`([ \t]+)-\s*mountPath:\s*/tmp/k8s-webhook-server/serving-certs[\s\S]*?readOnly:\s*true`)
		yamlContent = mountPattern.ReplaceAllStringFunc(yamlContent, makeYamlContent)
	}

	return yamlContent
}

// makeMetricsVolumesConditional makes metrics volumes conditional on certManager.enable AND metrics.enable
func (t *HelmTemplater) makeMetricsVolumesConditional(yamlContent string) string {
	// Make metrics volumes conditional on certManager.enable AND metrics.enable
	if strings.Contains(yamlContent, "metrics-certs") && strings.Contains(yamlContent, "secretName: metrics-server-cert") {
		// Match only spaces/tabs for indent to avoid consuming the newline
		volumePattern := regexp.MustCompile(`([ \t]+)-\s*name:\s*metrics-certs[\s\S]*?secretName:\s*metrics-server-cert`)
		yamlContent = volumePattern.ReplaceAllStringFunc(yamlContent, func(match string) string {
			lines := strings.Split(match, "\n")
			if len(lines) > 0 {
				indent := ""
				if len(lines[0]) > 0 && lines[0][0] == ' ' {
					// Count leading spaces
					for _, char := range lines[0] {
						if char == ' ' {
							indent += " "
						} else {
							break
						}
					}
				}

				// Reconstruct the block with conditional wrapper
				var result strings.Builder
				result.WriteString(fmt.Sprintf("%s{{- if and .Values.certManager.enable .Values.metrics.enable }}\n", indent))
				for _, line := range lines {
					result.WriteString(line + "\n")
				}
				result.WriteString(fmt.Sprintf("%s{{- end }}", indent))
				return result.String()
			}
			return match
		})
	}

	return yamlContent
}

// injectCommonLabels adds a Helm template snippet to append user-provided common labels
// (.Values.commonLabels) to every metadata.labels block while preserving indentation.
// It avoids duplicate insertion by checking for an existing snippet nearby.
// no common labels injection; labels come from kustomize manifests

// makeMetricsVolumeMountsConditional makes metrics volumeMounts conditional on certManager.enable AND metrics.enable
func (t *HelmTemplater) makeMetricsVolumeMountsConditional(yamlContent string) string {
	// Make metrics volumeMounts conditional on certManager.enable AND metrics.enable
	metricsCertsPath := "/tmp/k8s-metrics-server/metrics-certs"
	if strings.Contains(yamlContent, "metrics-certs") && strings.Contains(yamlContent, metricsCertsPath) {
		// Match only spaces/tabs for indent to avoid consuming the newline
		mountPattern := regexp.MustCompile(
			`([ \t]+)-\s*mountPath:\s*/tmp/k8s-metrics-server/metrics-certs[\s\S]*?readOnly:\s*true`)
		yamlContent = mountPattern.ReplaceAllStringFunc(yamlContent, func(match string) string {
			lines := strings.Split(match, "\n")
			if len(lines) > 0 {
				indent := ""
				if len(lines[0]) > 0 && lines[0][0] == ' ' {
					// Count leading spaces
					for _, char := range lines[0] {
						if char == ' ' {
							indent += " "
						} else {
							break
						}
					}
				}

				// Reconstruct the block with conditional wrapper
				var result strings.Builder
				result.WriteString(fmt.Sprintf("%s{{- if and .Values.certManager.enable .Values.metrics.enable }}\n", indent))
				for _, line := range lines {
					result.WriteString(line + "\n")
				}
				result.WriteString(fmt.Sprintf("%s{{- end }}", indent))
				return result.String()
			}
			return match
		})
	}

	return yamlContent
}

// injectCRDResourcePolicyAnnotation adds the helm.sh/resource-policy: keep annotation
// to CRDs conditionally based on .Values.crd.keep. This prevents CRDs from being deleted
// on helm uninstall when crd.keep is true in values.yaml.
func (t *HelmTemplater) injectCRDResourcePolicyAnnotation(yamlContent string) string {
	// Check if metadata section exists
	if !strings.Contains(yamlContent, "metadata:") {
		return yamlContent
	}

	lines := strings.Split(yamlContent, "\n")

	// Check if annotations: already exists
	if strings.Contains(yamlContent, "annotations:") {
		// Find the annotations: line and determine its indentation
		for i, line := range lines {
			trimmed := strings.TrimSpace(line)
			if trimmed == "annotations:" || strings.HasPrefix(trimmed, "annotations:") {
				annotationsIndent, _ := leadingWhitespace(line)
				// Annotation values need one more level of indentation (2 spaces for sigs.k8s.io/yaml)
				valueIndent := annotationsIndent + "  "

				// Build the conditional annotation block
				resourcePolicyBlock := fmt.Sprintf(
					"%s{{- if .Values.crd.keep }}\n%s\"helm.sh/resource-policy\": keep\n%s{{- end }}",
					valueIndent, valueIndent, valueIndent)

				// Insert after the annotations: line
				result := make([]string, 0, len(lines)+3)
				result = append(result, lines[:i+1]...)
				result = append(result, resourcePolicyBlock)
				result = append(result, lines[i+1:]...)
				return strings.Join(result, "\n")
			}
		}
	} else {
		// No annotations section exists, need to add it after metadata:
		for i, line := range lines {
			trimmed := strings.TrimSpace(line)
			if trimmed == "metadata:" || strings.HasPrefix(trimmed, "metadata:") {
				metadataIndent, _ := leadingWhitespace(line)
				// Fields under metadata need one more level of indentation (2 spaces for sigs.k8s.io/yaml)
				fieldIndent := metadataIndent + "  "
				// Annotation values need two more levels (4 spaces total)
				valueIndent := metadataIndent + "    "

				// Build annotations section with conditional resource-policy
				annotationsSection := fmt.Sprintf(
					"%sannotations:\n%s{{- if .Values.crd.keep }}\n%s\"helm.sh/resource-policy\": keep\n%s{{- end }}",
					fieldIndent, valueIndent, valueIndent, valueIndent)

				// Insert after the metadata: line
				result := make([]string, 0, len(lines)+4)
				result = append(result, lines[:i+1]...)
				result = append(result, annotationsSection)
				result = append(result, lines[i+1:]...)
				return strings.Join(result, "\n")
			}
		}
	}

	return yamlContent
}

// addConditionalWrappers adds conditional Helm logic based on resource type
func (t *HelmTemplater) addConditionalWrappers(yamlContent string, resource *unstructured.Unstructured) string {
	kind := resource.GetKind()
	apiVersion := resource.GetAPIVersion()
	name := resource.GetName()

	switch {
	case kind == kindNamespace:
		return ""
	case kind == "CustomResourceDefinition":
		// CRDs need resource-policy annotation for helm uninstall protection
		yamlContent = t.injectCRDResourcePolicyAnnotation(yamlContent)
		// CRDs need crd.enable condition
		return fmt.Sprintf("{{- if .Values.crd.enable }}\n%s{{- end }}\n", yamlContent)
	case kind == kindCertificate && apiVersion == apiVersionCertManager:
		// Handle different certificate types
		if strings.Contains(name, "metrics-cert") || strings.Contains(name, "metrics") {
			// Metrics certificates need both certManager and metrics enabled
			return fmt.Sprintf("{{- if and .Values.certManager.enable .Values.metrics.enable }}\n%s{{- end }}\n",
				yamlContent)
		}
		// Other certificates (webhook serving certs) only need certManager enabled
		return fmt.Sprintf("{{- if .Values.certManager.enable }}\n%s{{- end }}", yamlContent)
	case kind == kindIssuer && apiVersion == apiVersionCertManager:
		// All cert-manager issuers need certManager enabled
		return fmt.Sprintf("{{- if .Values.certManager.enable }}\n%s{{- end }}", yamlContent)
	case kind == kindServiceMonitor && apiVersion == apiVersionMonitoring:
		// ServiceMonitors need prometheus enabled
		return fmt.Sprintf("{{- if .Values.prometheus.enable }}\n%s{{- end }}", yamlContent)
	case kind == kindServiceAccount || kind == kindRole || kind == kindClusterRole ||
		kind == kindRoleBinding || kind == kindClusterRoleBinding:
		// Distinguish between essential RBAC and helper RBAC
		if strings.Contains(name, "admin-role") || strings.Contains(name, "editor-role") ||
			strings.Contains(name, "viewer-role") {
			// Helper RBAC roles (admin/editor/viewer) - convenience roles for CRD management
			return fmt.Sprintf("{{- if .Values.rbacHelpers.enable }}\n%s{{- end }}\n", yamlContent)
		}
		if strings.Contains(name, "metrics") {
			// Metrics RBAC depends on metrics being enabled
			return fmt.Sprintf("{{- if .Values.metrics.enable }}\n%s{{- end }}\n", yamlContent)
		}
		// Essential RBAC (controller-manager, leader-election, manager roles) - always enabled
		// These are required for the controller to function properly
		return yamlContent
	case kind == kindValidatingWebhook || kind == kindMutatingWebhook:
		// Webhook configurations should be conditional on webhook.enable
		yamlContent = t.makeWebhookAnnotationsConditional(yamlContent)
		return fmt.Sprintf("{{- if .Values.webhook.enable }}\n%s{{- end }}\n", yamlContent)
	case kind == kindService:
		// Services need conditional logic based on their purpose
		if strings.Contains(name, "metrics") {
			// Metrics services need metrics enabled
			return fmt.Sprintf("{{- if .Values.metrics.enable }}\n%s{{- end }}\n", yamlContent)
		}
		if strings.Contains(name, "webhook") {
			// Webhook services need webhook enabled
			return fmt.Sprintf("{{- if .Values.webhook.enable }}\n%s{{- end }}\n", yamlContent)
		}
		// Other services don't need conditionals
		return yamlContent
	default:
		// No conditional wrapper needed for other resources (Deployment, Namespace)
		return yamlContent
	}
}

// collapseBlankLineAfterIf removes a single empty line that may appear
// immediately after a Helm if directive line, e.g. `{{- if ... }}`.
// This keeps templates compact and matches expected formatting in tests.
func (t *HelmTemplater) collapseBlankLineAfterIf(yamlContent string) string {
	lines := strings.Split(yamlContent, "\n")
	if len(lines) == 0 {
		return yamlContent
	}
	out := make([]string, 0, len(lines))
	for i := 0; i < len(lines); i++ {
		line := lines[i]
		// If current line is an if, and next line is blank, skip the blank
		if strings.Contains(line, "{{- if ") {
			out = append(out, line)
			if i+1 < len(lines) && strings.TrimSpace(lines[i+1]) == "" {
				i++ // skip one blank line after if
			}
			continue
		}
		// If current line is blank, and next line is an end, skip the blank
		if strings.TrimSpace(line) == "" && i+1 < len(lines) && strings.Contains(lines[i+1], "{{- end }}") {
			continue
		}
		out = append(out, line)
	}
	return strings.Join(out, "\n")
}

// templatePorts replaces hardcoded port values with Helm template references
// This makes ports configurable via values.yaml under webhook.port and metrics.port
func (t *HelmTemplater) templatePorts(yamlContent string, resource *unstructured.Unstructured) string {
	resourceName := resource.GetName()

	// Determine if this is a webhook-related resource
	isWebhook := strings.Contains(resourceName, "webhook")

	// Determine if this is a metrics-related resource
	isMetrics := strings.Contains(resourceName, "metrics")

	// For Deployments, check for webhook ports in the content
	if resource.GetKind() == kindDeployment {
		// Check if this deployment has webhook-server ports
		if strings.Contains(yamlContent, "webhook-server") || strings.Contains(yamlContent, "name: webhook") {
			isWebhook = true
		}
	}

	// Template webhook ports (9443 by default)
	if isWebhook {
		// Replace containerPort: 9443 (or any value) for webhook-server with template
		if strings.Contains(yamlContent, "webhook-server") {
			yamlContent = regexp.MustCompile(`(?m)(\s*- )?containerPort:\s*\d+(\s*\n\s*name:\s*webhook-server)`).
				ReplaceAllString(yamlContent, "${1}containerPort: {{ .Values.webhook.port }}${2}")
		}

		// Replace targetPort: 9443 with webhook.port template
		yamlContent = regexp.MustCompile(`(\s*)targetPort:\s*9443`).
			ReplaceAllString(yamlContent, "${1}targetPort: {{ .Values.webhook.port }}")
	}

	// Template metrics ports (8443 by default)
	if isMetrics {
		// Replace port: 8443 with metrics.port template
		yamlContent = regexp.MustCompile(`(\s*)port:\s*8443`).
			ReplaceAllString(yamlContent, "${1}port: {{ .Values.metrics.port }}")

		// Replace targetPort: 8443 with metrics.port template
		yamlContent = regexp.MustCompile(`(\s*)targetPort:\s*8443`).
			ReplaceAllString(yamlContent, "${1}targetPort: {{ .Values.metrics.port }}")
	}

	// Template port-related arguments in Deployment
	if resource.GetKind() == kindDeployment {
		// Replace --metrics-bind-address=:8443 with templated version
		yamlContent = regexp.MustCompile(`--metrics-bind-address=:[0-9]+`).
			ReplaceAllString(yamlContent, "--metrics-bind-address=:{{ .Values.metrics.port }}")

		// Replace --webhook-port=9443 with templated version (if present)
		yamlContent = regexp.MustCompile(`--webhook-port=[0-9]+`).
			ReplaceAllString(yamlContent, "--webhook-port={{ .Values.webhook.port }}")
	}

	return yamlContent
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/helm_templater_test.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

const (
	// Test expectation constants for test-project.resourceName templates
	expectedIssuerName = `name: {{ include "test-project.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }}`
)

var _ = Describe("HelmTemplater", func() {
	var templater *HelmTemplater

	BeforeEach(func() {
		templater = &HelmTemplater{
			detectedPrefix:   "test-project",
			chartName:        "test-project",
			managerNamespace: "test-project-system",
		}
	})

	// No global labels injection is performed by v2-alpha

	Context("basic template processing", func() {
		It("should replace kustomize managed-by labels with Helm equivalents", func() {
			deploymentResource := &unstructured.Unstructured{}
			deploymentResource.SetAPIVersion("apps/v1")
			deploymentResource.SetKind("Deployment")
			deploymentResource.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: test-project
    control-plane: controller-manager
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  template:
    metadata:
      labels:
        control-plane: controller-manager`

			result := templater.ApplyHelmSubstitutions(content, deploymentResource)

			// Should replace kustomize managed-by with Helm template
			Expect(result).To(ContainSubstring("app.kubernetes.io/managed-by: {{ .Release.Service }}"))
			// Should replace app.kubernetes.io/name with chart name template
			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).To(ContainSubstring("control-plane: controller-manager"))

			// Should substitute namespace
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))

			// Should NOT add extra Helm metadata injection
			Expect(result).NotTo(ContainSubstring(`{{- include "chart.labels"`))
			Expect(result).NotTo(ContainSubstring(`{{- include "chart.annotations"`))
		})

		It("should handle cert-manager annotations with proper indentation", func() {
			resource := &unstructured.Unstructured{}
			resource.SetAPIVersion("admissionregistration.k8s.io/v1")
			resource.SetKind("ValidatingWebhookConfiguration")
			resource.SetName("test-project-validating-webhook-configuration")

			content := `apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: test-project-system/test-project-serving-cert
  name: test-project-validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1`

			result := templater.ApplyHelmSubstitutions(content, resource)

			// Should have proper conditional formatting without extra spaces
			Expect(result).To(ContainSubstring("{{- if .Values.certManager.enable }}"))
			Expect(result).To(ContainSubstring("cert-manager.io/inject-ca-from:"))
			Expect(result).To(ContainSubstring("{{- end }}"))

			// Should NOT have extra blank lines or improper indentation
			Expect(result).NotTo(ContainSubstring("{{- if .Values.certManager.enable }}\n\n"))
			Expect(result).NotTo(ContainSubstring("cert-manager.io/inject-ca-from:\n\n"))
		})

		It("should template deployment spec.replicas from .Values.manager.replicas", func() {
			deploymentResource := &unstructured.Unstructured{}
			deploymentResource.SetAPIVersion("apps/v1")
			deploymentResource.SetKind("Deployment")
			deploymentResource.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    spec:
      containers:
      - name: manager
        image: controller:latest
`
			result := templater.ApplyHelmSubstitutions(content, deploymentResource)
			Expect(result).To(ContainSubstring("replicas: {{ .Values.manager.replicas }}"))
			Expect(result).NotTo(ContainSubstring("replicas: 1"))
		})

		It("should handle container args with proper indentation", func() {
			deploymentResource := &unstructured.Unstructured{}
			deploymentResource.SetAPIVersion("apps/v1")
			deploymentResource.SetKind("Deployment")
			deploymentResource.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - args:
        - --metrics-bind-address=:8443
        - --health-probe-bind-address=:8081
        - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs/tls.crt
        - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs/tls.crt
        - --leader-elect
        env:
        - name: BUSYBOX_IMAGE
          value: busybox:1.36.1
        image: controller:latest
        imagePullPolicy: IfNotPresent
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        name: manager
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      serviceAccountName: test-project-controller-manager`

			result := templater.ApplyHelmSubstitutions(content, deploymentResource)

			Expect(result).To(ContainSubstring("{{- if .Values.metrics.enable }}"))
			Expect(result).To(ContainSubstring("- --metrics-bind-address=:{{ .Values.metrics.port }}"))
			Expect(result).To(ContainSubstring("- --metrics-bind-address=0"))
			Expect(result).To(ContainSubstring("- --health-probe-bind-address=:8081"))
			Expect(result).To(ContainSubstring("{{- range .Values.manager.args }}"))
			Expect(result).NotTo(ContainSubstring("BUSYBOX_IMAGE"))
			Expect(result).NotTo(ContainSubstring("MEMCACHED_IMAGE"))
			Expect(result).To(ContainSubstring("image: " +
				"\"{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}\""))
			Expect(result).To(ContainSubstring("imagePullPolicy: {{ .Values.manager.image.pullPolicy }}"))
			Expect(result).NotTo(ContainSubstring("controller:latest"))
		})

		It("should handle volume mounts with proper indentation", func() {
			deploymentResource := &unstructured.Unstructured{}
			deploymentResource.SetAPIVersion("apps/v1")
			deploymentResource.SetKind("Deployment")
			deploymentResource.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - volumeMounts:
        - mountPath: /tmp/k8s-webhook-server/serving-certs
          name: webhook-certs
          readOnly: true
        - mountPath: /tmp/k8s-metrics-server/metrics-certs
          name: metrics-certs
          readOnly: true`

			result := templater.ApplyHelmSubstitutions(content, deploymentResource)

			// Should have conditional blocks for webhook certs
			Expect(result).To(ContainSubstring("{{- if .Values.certManager.enable }}"))
			Expect(result).To(ContainSubstring("mountPath: /tmp/k8s-webhook-server/serving-certs"))

			// Should have conditional blocks for metrics certs
			Expect(result).To(ContainSubstring("{{- if and .Values.certManager.enable .Values.metrics.enable }}"))
			Expect(result).To(ContainSubstring("mountPath: /tmp/k8s-metrics-server/metrics-certs"))
		})

		It("should handle namespace substitution correctly", func() {
			serviceResource := &unstructured.Unstructured{}
			serviceResource.SetAPIVersion("v1")
			serviceResource.SetKind("Service")
			serviceResource.SetName("test-project-webhook-service")

			content := `apiVersion: v1
kind: Service
metadata:
  name: test-project-webhook-service
  namespace: test-project-system
spec:
  type: ClusterIP`

			result := templater.ApplyHelmSubstitutions(content, serviceResource)

			// Should substitute namespace with Helm template
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))
			Expect(result).NotTo(ContainSubstring("namespace: test-project-system"))
		})

		It("should preserve annotations without modification", func() {
			webhookResource := &unstructured.Unstructured{}
			webhookResource.SetAPIVersion("admissionregistration.k8s.io/v1")
			webhookResource.SetKind("ValidatingWebhookConfiguration")
			webhookResource.SetName("test-project-validating-webhook-configuration")

			content := `apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: test-system/test-serving-cert
  name: test-project-validating-webhook-configuration`

			result := templater.ApplyHelmSubstitutions(content, webhookResource)

			// Should preserve existing kustomize annotations as-is
			Expect(result).To(ContainSubstring("cert-manager.io/inject-ca-from"))

			// Should NOT add extra Helm metadata injection
			Expect(result).NotTo(ContainSubstring(`{{- include "chart.labels"`))
			Expect(result).NotTo(ContainSubstring(`{{- include "chart.annotations"`))
		})

		It("should template imagePullSecrets", func() {
			deploymentResource := &unstructured.Unstructured{}
			deploymentResource.SetAPIVersion("apps/v1")
			deploymentResource.SetKind("Deployment")
			deploymentResource.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      imagePullSecrets:
      - name: test-secret
      containers:
      - args:
        - --metrics-bind-address=:8443
        - --health-probe-bind-address=:8081
        - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs/tls.crt
        - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs/tls.crt
        - --leader-elect`

			result := templater.ApplyHelmSubstitutions(content, deploymentResource)

			Expect(result).To(ContainSubstring("imagePullSecrets:"))
			Expect(result).NotTo(ContainSubstring("test-secret"))
		})

		It("should template empty imagePullSecrets", func() {
			deploymentResource := &unstructured.Unstructured{}
			deploymentResource.SetAPIVersion("apps/v1")
			deploymentResource.SetKind("Deployment")
			deploymentResource.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      imagePullSecrets: []
      containers:
      - args:
        - --metrics-bind-address=:8443
        - --health-probe-bind-address=:8081
        - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs/tls.crt
        - --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs/tls.crt
        - --leader-elect`

			result := templater.ApplyHelmSubstitutions(content, deploymentResource)

			Expect(result).To(ContainSubstring("imagePullSecrets:"))
		})
	})

	Context("conditional wrapping", func() {
		It("should add metrics conditional for ServiceMonitor resources", func() {
			serviceMonitorResource := &unstructured.Unstructured{}
			serviceMonitorResource.SetAPIVersion("monitoring.coreos.com/v1")
			serviceMonitorResource.SetKind("ServiceMonitor")
			serviceMonitorResource.SetName("test-project-controller-manager-metrics-monitor")

			content := `apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: test-project-controller-manager-metrics-monitor`

			result := templater.ApplyHelmSubstitutions(content, serviceMonitorResource)

			// Should be wrapped with prometheus enable conditional
			Expect(result).To(ContainSubstring("{{- if .Values.prometheus.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
		})

		It("should add metrics conditional for metrics services", func() {
			serviceResource := &unstructured.Unstructured{}
			serviceResource.SetAPIVersion("v1")
			serviceResource.SetKind("Service")
			serviceResource.SetName("test-project-controller-manager-metrics-service")

			content := `apiVersion: v1
kind: Service
metadata:
  name: test-project-controller-manager-metrics-service`

			result := templater.ApplyHelmSubstitutions(content, serviceResource)

			// Should be wrapped with metrics enable conditional
			Expect(result).To(ContainSubstring("{{- if .Values.metrics.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
		})

		It("should add cert-manager conditional for Certificate resources", func() {
			certResource := &unstructured.Unstructured{}
			certResource.SetAPIVersion("cert-manager.io/v1")
			certResource.SetKind("Certificate")
			certResource.SetName("test-project-serving-cert")

			content := `apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: test-project-serving-cert`

			result := templater.ApplyHelmSubstitutions(content, certResource)

			// Should be wrapped with certManager enable conditional
			Expect(result).To(ContainSubstring("{{- if .Values.certManager.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
		})

		It("should add combined conditionals for metrics certificates", func() {
			certResource := &unstructured.Unstructured{}
			certResource.SetAPIVersion("cert-manager.io/v1")
			certResource.SetKind("Certificate")
			certResource.SetName("test-project-metrics-certs")

			content := `apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: test-project-metrics-certs`

			result := templater.ApplyHelmSubstitutions(content, certResource)

			// Should be wrapped with both metrics and certManager conditionals
			Expect(result).To(ContainSubstring("{{- if and .Values.certManager.enable .Values.metrics.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
		})

		It("should NOT add conditionals to essential resources", func() {
			// Test essential RBAC
			clusterRoleResource := &unstructured.Unstructured{}
			clusterRoleResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			clusterRoleResource.SetKind("ClusterRole")
			clusterRoleResource.SetName("test-project-manager-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-project-manager-role`

			result := templater.ApplyHelmSubstitutions(content, clusterRoleResource)

			// Should NOT wrap essential RBAC with conditionals
			Expect(result).NotTo(ContainSubstring("{{- if .Values"))
		})

		It("should add webhook conditional for webhook services", func() {
			serviceResource := &unstructured.Unstructured{}
			serviceResource.SetAPIVersion("v1")
			serviceResource.SetKind("Service")
			serviceResource.SetName("test-project-webhook-service")

			webhookContent := `apiVersion: v1
kind: Service
metadata:
  name: test-project-webhook-service`

			webhookResult := templater.ApplyHelmSubstitutions(webhookContent, serviceResource)

			// Should wrap webhook service with webhook.enable conditional
			Expect(webhookResult).To(ContainSubstring("{{- if .Values.webhook.enable }}"))
			Expect(webhookResult).To(ContainSubstring("{{- end }}"))
		})

		It("should add webhook conditional for webhook configurations", func() {
			mutatingWebhookResource := &unstructured.Unstructured{}
			mutatingWebhookResource.SetAPIVersion("admissionregistration.k8s.io/v1")
			mutatingWebhookResource.SetKind("MutatingWebhookConfiguration")
			mutatingWebhookResource.SetName("test-project-mutating-webhook-configuration")

			content := `apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: test-project-mutating-webhook-configuration`

			result := templater.ApplyHelmSubstitutions(content, mutatingWebhookResource)

			// Webhook configurations should be conditional on webhook.enable
			Expect(result).To(ContainSubstring("{{- if .Values.webhook.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
		})

		It("should add webhook conditional for validating webhook configurations", func() {
			validatingWebhookResource := &unstructured.Unstructured{}
			validatingWebhookResource.SetAPIVersion("admissionregistration.k8s.io/v1")
			validatingWebhookResource.SetKind("ValidatingWebhookConfiguration")
			validatingWebhookResource.SetName("test-project-validating-webhook-configuration")

			content := `apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: test-project-system/test-project-serving-cert
  name: test-project-validating-webhook-configuration`

			result := templater.ApplyHelmSubstitutions(content, validatingWebhookResource)

			// Webhook configurations should be wrapped with webhook.enable
			Expect(result).To(ContainSubstring("{{- if .Values.webhook.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
			// Cert-manager annotation should still be conditional on certManager.enable
			Expect(result).To(ContainSubstring("{{- if .Values.certManager.enable }}"))
		})

		It("should add crd.enable conditional and resource-policy annotation for CRDs", func() {
			crdResource := &unstructured.Unstructured{}
			crdResource.SetAPIVersion("apiextensions.k8s.io/v1")
			crdResource.SetKind("CustomResourceDefinition")
			crdResource.SetName("guestbooks.example.com")

			content := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: guestbooks.example.com
spec:
  group: example.com`

			result := templater.ApplyHelmSubstitutions(content, crdResource)

			// Should be wrapped with crd.enable conditional
			Expect(result).To(ContainSubstring("{{- if .Values.crd.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
			// Should have resource-policy annotation for helm uninstall protection
			Expect(result).To(ContainSubstring("{{- if .Values.crd.keep }}"))
			Expect(result).To(ContainSubstring(`"helm.sh/resource-policy": keep`))
			// Injected annotations should use 2-space indentation matching sigs.k8s.io/yaml output.
			// annotations: at 2-space indent, values at 4-space indent.
			expectedAnnotations := "  annotations:\n" +
				"    {{- if .Values.crd.keep }}\n" +
				"    \"helm.sh/resource-policy\": keep\n" +
				"    {{- end }}"
			Expect(result).To(ContainSubstring(expectedAnnotations))
		})

		It("should add resource-policy annotation to CRDs that already have annotations", func() {
			crdResource := &unstructured.Unstructured{}
			crdResource.SetAPIVersion("apiextensions.k8s.io/v1")
			crdResource.SetKind("CustomResourceDefinition")
			crdResource.SetName("configs.example.com")

			content := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.17.0
  name: configs.example.com
spec:
  group: example.com`

			result := templater.ApplyHelmSubstitutions(content, crdResource)

			// Should be wrapped with crd.enable conditional
			Expect(result).To(ContainSubstring("{{- if .Values.crd.enable }}"))
			// Should have resource-policy annotation
			Expect(result).To(ContainSubstring("{{- if .Values.crd.keep }}"))
			Expect(result).To(ContainSubstring(`"helm.sh/resource-policy": keep`))
			// Should preserve existing annotation
			Expect(result).To(ContainSubstring("controller-gen.kubebuilder.io/version"))
			// Injected annotation should be at same indent as existing annotations (4 spaces)
			expectedAnnotations := "  annotations:\n" +
				"    {{- if .Values.crd.keep }}\n" +
				"    \"helm.sh/resource-policy\": keep\n" +
				"    {{- end }}\n" +
				"    controller-gen.kubebuilder.io/version"
			Expect(result).To(ContainSubstring(expectedAnnotations))
		})
	})

	Context("helper RBAC wrapping", func() {
		It("should add rbacHelpers conditional for helper RBAC roles", func() {
			clusterRoleResource := &unstructured.Unstructured{}
			clusterRoleResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			clusterRoleResource.SetKind("ClusterRole")
			clusterRoleResource.SetName("test-project-memcached-editor-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-project-memcached-editor-role`

			result := templater.ApplyHelmSubstitutions(content, clusterRoleResource)

			// Should be wrapped with rbacHelpers conditional
			Expect(result).To(ContainSubstring("{{- if .Values.rbacHelpers.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
		})

		It("should add rbacHelpers conditional for helper ClusterRoleBindings", func() {
			bindingResource := &unstructured.Unstructured{}
			bindingResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			bindingResource.SetKind("ClusterRoleBinding")
			bindingResource.SetName("test-project-memcached-viewer-rolebinding")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-project-memcached-viewer-rolebinding`

			result := templater.ApplyHelmSubstitutions(content, bindingResource)

			// Should be wrapped with rbacHelpers conditional
			Expect(result).To(ContainSubstring("{{- if .Values.rbacHelpers.enable }}"))
			Expect(result).To(ContainSubstring("{{- end }}"))
		})
	})

	Context("chart.fullname templating", func() {
		It("should template resource names with test-project.resourceName for proper truncation", func() {
			serviceAccountResource := &unstructured.Unstructured{}
			serviceAccountResource.SetAPIVersion("v1")
			serviceAccountResource.SetKind("ServiceAccount")
			serviceAccountResource.SetName("test-project-controller-manager")

			content := `apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-project-controller-manager
  namespace: test-project-system`

			result := templater.ApplyHelmSubstitutions(content, serviceAccountResource)

			// Should template with test-project.resourceName which handles 63-char truncation
			expected := `name: {{ include "test-project.resourceName" (dict "suffix" "controller-manager" "context" $) }}`
			Expect(result).To(ContainSubstring(expected))
			Expect(result).NotTo(ContainSubstring("name: test-project-controller-manager"))
		})
		It("should template ServiceMonitor name with test-project.resourceName for proper truncation", func() {
			serviceMonitorResource := &unstructured.Unstructured{}
			serviceMonitorResource.SetAPIVersion("monitoring.coreos.com/v1")
			serviceMonitorResource.SetKind("ServiceMonitor")
			serviceMonitorResource.SetName("test-project-controller-manager-metrics-monitor")

			content := `apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: test-project-controller-manager-metrics-monitor`

			result := templater.ApplyHelmSubstitutions(content, serviceMonitorResource)

			// Should template with test-project.resourceName which handles 63-char truncation
			expected := `name: {{ include "test-project.resourceName" ` +
				`(dict "suffix" "controller-manager-metrics-monitor" "context" $) }}`
			Expect(result).To(ContainSubstring(expected))
			Expect(result).NotTo(ContainSubstring("name: test-project-controller-manager-metrics-monitor"))
		})
	})

	Context("app.kubernetes.io/name label templating", func() {
		It("should template app.kubernetes.io/name for Deployment", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: test-project
    control-plane: controller-manager`

			result := templater.ApplyHelmSubstitutions(content, deployment)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for Service", func() {
			service := &unstructured.Unstructured{}
			service.SetAPIVersion("v1")
			service.SetKind("Service")
			service.SetName("test-project-webhook-service")

			content := `apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, service)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for ServiceAccount", func() {
			sa := &unstructured.Unstructured{}
			sa.SetAPIVersion("v1")
			sa.SetKind("ServiceAccount")
			sa.SetName("test-project-controller-manager")

			content := `apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, sa)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for ClusterRole", func() {
			clusterRole := &unstructured.Unstructured{}
			clusterRole.SetAPIVersion("rbac.authorization.k8s.io/v1")
			clusterRole.SetKind("ClusterRole")
			clusterRole.SetName("test-project-manager-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, clusterRole)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for Role", func() {
			role := &unstructured.Unstructured{}
			role.SetAPIVersion("rbac.authorization.k8s.io/v1")
			role.SetKind("Role")
			role.SetName("test-project-leader-election-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, role)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for RoleBinding", func() {
			rb := &unstructured.Unstructured{}
			rb.SetAPIVersion("rbac.authorization.k8s.io/v1")
			rb.SetKind("RoleBinding")
			rb.SetName("test-project-leader-election-rolebinding")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, rb)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for ClusterRoleBinding", func() {
			crb := &unstructured.Unstructured{}
			crb.SetAPIVersion("rbac.authorization.k8s.io/v1")
			crb.SetKind("ClusterRoleBinding")
			crb.SetName("test-project-manager-rolebinding")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, crb)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for Certificate", func() {
			cert := &unstructured.Unstructured{}
			cert.SetAPIVersion("cert-manager.io/v1")
			cert.SetKind("Certificate")
			cert.SetName("test-project-serving-cert")

			content := `apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, cert)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template app.kubernetes.io/name for Issuer", func() {
			issuer := &unstructured.Unstructured{}
			issuer.SetAPIVersion("cert-manager.io/v1")
			issuer.SetKind("Issuer")
			issuer.SetName("test-project-selfsigned-issuer")

			content := `apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, issuer)

			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should handle label already templated without breaking", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: {{ include "test-project.name" . }}
    control-plane: controller-manager`

			result := templater.ApplyHelmSubstitutions(content, deployment)

			// Should keep the template as-is
			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).ToNot(ContainSubstring("app.kubernetes.io/name: test-project"))
		})

		It("should template multiple occurrences in same resource", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/name: test-project
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: test-project
  template:
    metadata:
      labels:
        app.kubernetes.io/name: test-project`

			result := templater.ApplyHelmSubstitutions(content, deployment)

			// All three should be templated
			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: test-project"))
			// Count occurrences - should be 3
			count := strings.Count(result, "app.kubernetes.io/name: {{ include \"test-project.name\" . }}")
			Expect(count).To(Equal(3))
		})
	})

	Context("existing Go template syntax escaping", func() {
		It("should escape existing Go template syntax in CRD samples", func() {
			crdResource := &unstructured.Unstructured{}
			crdResource.SetAPIVersion("apiextensions.k8s.io/v1")
			crdResource.SetKind("CustomResourceDefinition")
			crdResource.SetName("changetransferpolicies.promoter.argoproj.io")

			content := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: changetransferpolicies.promoter.argoproj.io
spec:
  names:
    kind: ChangeTransferPolicy
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              pullRequest:
                properties:
                  template:
                    properties:
                      description:
                        default: "Promoting {{ .ChangeTransferPolicy.Spec.ActiveBranch }}"
                        type: string
                      title:
                        default: "Promote {{ trunc 5 .ChangeTransferPolicy.Status.Proposed.Dry.Sha }}"
                        type: string`

			result := templater.ApplyHelmSubstitutions(content, crdResource)

			// Existing {{ }} should be escaped to {{ "{{ ... }}" }}
			Expect(result).To(ContainSubstring(`{{ "{{ .ChangeTransferPolicy.Spec.ActiveBranch }}" }}`),
				"existing template syntax should be escaped")
			Expect(result).To(ContainSubstring(`{{ "{{ trunc 5 .ChangeTransferPolicy.Status.Proposed.Dry.Sha }}" }}`),
				"function calls in templates should be escaped")

			// Should NOT have unescaped Go template syntax (which would break Helm)
			// We check that all ChangeTransferPolicy references are properly wrapped
			// Pattern checks for: default: "...{{ .ChangeTransferPolicy" (not escaped)
			// The properly escaped version is: default: "...{{ "{{ .ChangeTransferPolicy..." }}"
			Expect(result).NotTo(MatchRegexp(`default:\s+"[^{]*\{\{\s*\.ChangeTransferPolicy`),
				"unescaped Go templates should not exist in default values")
		})

		It("should escape multiple template expressions on the same line", func() {
			crdResource := &unstructured.Unstructured{}
			crdResource.SetAPIVersion("apiextensions.k8s.io/v1")
			crdResource.SetKind("CustomResourceDefinition")
			crdResource.SetName("policies.example.com")

			content := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
spec:
  versions:
  - schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              message:
                default: "From {{ .Source.Branch }} to {{ .Target.Branch }}"`

			result := templater.ApplyHelmSubstitutions(content, crdResource)

			// Both templates should be escaped (applies to all resources)
			Expect(result).To(ContainSubstring(`{{ "{{ .Source.Branch }}" }}`))
			Expect(result).To(ContainSubstring(`{{ "{{ .Target.Branch }}" }}`))
		})

		It("should escape templates with special characters", func() {
			crdResource := &unstructured.Unstructured{}
			crdResource.SetAPIVersion("apiextensions.k8s.io/v1")
			crdResource.SetKind("CustomResourceDefinition")
			crdResource.SetName("configs.example.com")

			content := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
spec:
  versions:
  - schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              value:
                default: "Value: {{ .Config.Key-With-Dashes }}"`

			result := templater.ApplyHelmSubstitutions(content, crdResource)

			Expect(result).To(ContainSubstring(`{{ "{{ .Config.Key-With-Dashes }}" }}`))
		})

		It("should handle template syntax with quotes correctly", func() {
			crdResource := &unstructured.Unstructured{}
			crdResource.SetAPIVersion("apiextensions.k8s.io/v1")
			crdResource.SetKind("CustomResourceDefinition")
			crdResource.SetName("messages.example.com")

			content := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
spec:
  versions:
  - schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              template:
                default: '{{ .Config.Message "default" }}'`

			result := templater.ApplyHelmSubstitutions(content, crdResource)

			// Quotes inside templates should be escaped
			Expect(result).To(ContainSubstring(`{{ "{{ .Config.Message \"default\" }}" }}`))
		})

		It("should not double-escape quotes already escaped by yaml.Marshal in double-quoted YAML scalars", func() {
			// Regression test: yaml.Marshal represents literal " inside a {{ }} expression as \"
			// in a double-quoted YAML scalar, so a second pass escaping " → \" produced \\" which
			// broke Helm's template parser by closing the string literal early (U+002D '-' error).
			crdResource := &unstructured.Unstructured{}
			crdResource.SetAPIVersion("apiextensions.k8s.io/v1")
			crdResource.SetKind("CustomResourceDefinition")
			crdResource.SetName("webrequestcommitstatuses.promoter.argoproj.io")

			// Simulate the raw YAML text that yaml.Marshal produces for a double-quoted scalar
			// containing a " character – the inner " appears as \" in the YAML text.
			content := `apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
spec:
  versions:
  - schema:
      openAPIV3Schema:
        properties:
          spec:
            description: "example: {{ index .NamespaceMetadata.Labels \"asset-id\" }}"`

			result := templater.ApplyHelmSubstitutions(content, crdResource)

			// The escaped form must be valid Go template syntax: \" (single backslash+quote),
			// NOT \\" (double backslash+quote) which would terminate the string literal early.
			Expect(result).To(ContainSubstring(`{{ "{{ index .NamespaceMetadata.Labels \"asset-id\" }}" }}`),
				"pre-escaped YAML quotes must not be double-escaped to \\\\ which breaks Helm template parsing")
			Expect(result).NotTo(ContainSubstring(`\\"asset-id\\"`),
				"double-escaped quotes (\\\\\") must not appear in the output")
		})

		It("should escape templates in ConfigMaps and other non-CRD resources", func() {
			configMapResource := &unstructured.Unstructured{}
			configMapResource.SetAPIVersion("v1")
			configMapResource.SetKind("ConfigMap")
			configMapResource.SetName("template-config")
			configMapResource.SetNamespace("test-project-system")

			// ANY resource can have Go template syntax that needs escaping
			// Examples: ConfigMaps with notification templates, Secrets with webhook URLs,
			// Deployment annotations with CI/CD metadata, etc.
			content := `apiVersion: v1
kind: ConfigMap
metadata:
  name: template-config
  namespace: test-project-system
  labels:
    app.kubernetes.io/name: test-project
data:
  notification: "Deployed from {{ .Source.Branch }} to {{ .Target.Branch }}"`

			result := templater.ApplyHelmSubstitutions(content, configMapResource)

			// Existing templates should be escaped (applies to ALL resources, not just CRDs)
			Expect(result).To(ContainSubstring(`{{ "{{ .Source.Branch }}" }}`))
			Expect(result).To(ContainSubstring(`{{ "{{ .Target.Branch }}" }}`))

			// Helm templates should still be added normally
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))
			Expect(result).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`))
		})

		It("should handle content without any templates", func() {
			configMapResource := &unstructured.Unstructured{}
			configMapResource.SetAPIVersion("v1")
			configMapResource.SetKind("ConfigMap")
			configMapResource.SetName("no-template")

			content := `apiVersion: v1
kind: ConfigMap
data:
  message: "No templates here"`

			result := templater.ApplyHelmSubstitutions(content, configMapResource)

			// Should not add any escaping
			Expect(result).To(ContainSubstring(`message: "No templates here"`))
			Expect(result).NotTo(ContainSubstring(`{{ "{{`))
		})
	})

	Context("edge cases", func() {
		It("should handle empty content", func() {
			testResource := &unstructured.Unstructured{}
			testResource.SetKind("ConfigMap")

			result := templater.ApplyHelmSubstitutions("", testResource)
			Expect(result).To(BeEmpty())
		})

		It("should handle resources without namespace", func() {
			clusterRoleResource := &unstructured.Unstructured{}
			clusterRoleResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			clusterRoleResource.SetKind("ClusterRole")
			clusterRoleResource.SetName("test-project-manager-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-project-manager-role`

			result := templater.ApplyHelmSubstitutions(content, clusterRoleResource)

			// Should not add namespace substitution for cluster-scoped resources
			Expect(result).NotTo(ContainSubstring("namespace:"))
		})

		It("should handle malformed YAML gracefully", func() {
			testResource := &unstructured.Unstructured{}
			testResource.SetKind("ConfigMap")

			malformedContent := "not: valid: yaml: content:"
			result := templater.ApplyHelmSubstitutions(malformedContent, testResource)

			// Should return content as-is for malformed YAML
			Expect(result).To(Equal(malformedContent))
		})
	})

	Context("namespace-scoped RBAC resources", func() {
		It("should preserve explicit namespace in Role for cross-namespace permissions", func() {
			roleResource := &unstructured.Unstructured{}
			roleResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			roleResource.SetKind("Role")
			roleResource.SetName("test-project-manager-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-manager-role
  namespace: infrastructure
  labels:
    app.kubernetes.io/name: test-project
rules:
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - get
  - list
  - patch
  - update
  - watch`

			result := templater.ApplyHelmSubstitutions(content, roleResource)

			// Namespace should be preserved (not templated) for cross-namespace permissions
			Expect(result).To(ContainSubstring("namespace: infrastructure"),
				"explicit namespace should be preserved for cross-namespace Role")
			Expect(result).NotTo(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"explicit namespace should NOT be templated to Release.Namespace")

			// Labels should still be templated
			Expect(result).To(ContainSubstring(`app.kubernetes.io/name: {{ include "test-project.name" . }}`))

			// Name should be templated
			Expect(result).To(ContainSubstring(`name: {{ include "test-project.resourceName"`))

			// Rules should be preserved
			Expect(result).To(ContainSubstring("- apps"))
			Expect(result).To(ContainSubstring("- deployments"))
		})

		It("should preserve explicit namespace in Role for leader election", func() {
			roleResource := &unstructured.Unstructured{}
			roleResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			roleResource.SetKind("Role")
			roleResource.SetName("test-project-leader-election-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-leader-election-role
  namespace: production
  labels:
    app.kubernetes.io/name: test-project
rules:
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
  - update`

			result := templater.ApplyHelmSubstitutions(content, roleResource)

			// Namespace should be preserved for cross-namespace leader election
			Expect(result).To(ContainSubstring("namespace: production"),
				"explicit namespace should be preserved for cross-namespace leader election Role")

			// Verify leader election permissions
			Expect(result).To(ContainSubstring("- coordination.k8s.io"))
			Expect(result).To(ContainSubstring("- leases"))
			Expect(result).To(ContainSubstring("- events"))
		})

		It("should preserve explicit namespace in RoleBinding metadata", func() {
			roleBindingResource := &unstructured.Unstructured{}
			roleBindingResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			roleBindingResource.SetKind("RoleBinding")
			roleBindingResource.SetName("test-project-manager-rolebinding")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-manager-rolebinding
  namespace: infrastructure
  labels:
    app.kubernetes.io/name: test-project
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system`

			result := templater.ApplyHelmSubstitutions(content, roleBindingResource)

			// RoleBinding metadata namespace should be preserved
			Expect(result).To(ContainSubstring("metadata:\n  name:"))
			Expect(result).To(ContainSubstring("namespace: infrastructure"),
				"RoleBinding metadata namespace should be preserved")

			// Subject namespace should be templated (references the controller namespace)
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"subject namespace should be templated to Release.Namespace")

			// Name references should be templated
			Expect(result).To(ContainSubstring(`name: {{ include "test-project.resourceName"`))
		})

		It("should template Role namespace when it matches project namespace", func() {
			roleResource := &unstructured.Unstructured{}
			roleResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			roleResource.SetKind("Role")
			roleResource.SetName("test-project-leader-election-role")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-leader-election-role
  namespace: test-project-system
  labels:
    app.kubernetes.io/name: test-project
rules:
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete`

			result := templater.ApplyHelmSubstitutions(content, roleResource)

			// When namespace matches project namespace, it should be templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"Role namespace should be templated when it matches project namespace")
			Expect(result).NotTo(ContainSubstring("namespace: test-project-system"),
				"project namespace should be templated, not preserved")
		})

		It("should handle RoleBinding with multiple subjects correctly", func() {
			roleBindingResource := &unstructured.Unstructured{}
			roleBindingResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			roleBindingResource.SetKind("RoleBinding")
			roleBindingResource.SetName("test-project-manager-rolebinding")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-manager-rolebinding
  namespace: infrastructure
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
- kind: ServiceAccount
  name: test-project-webhook
  namespace: test-project-system`

			result := templater.ApplyHelmSubstitutions(content, roleBindingResource)

			// Both subject namespaces should be templated
			subjectNamespaceCount := strings.Count(result, "namespace: {{ .Release.Namespace }}")
			Expect(subjectNamespaceCount).To(BeNumerically(">=", 2),
				"both subject namespaces should be templated")

			// RoleBinding metadata namespace should be preserved
			Expect(result).To(ContainSubstring("namespace: infrastructure"))
		})

		It("should preserve resource names when namespace appears as substring", func() {
			// Critical: namespace "user" must NOT break resource name "users"
			// This validates field-aware replacement prevents substring corruption
			roleResource := &unstructured.Unstructured{}
			roleResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			roleResource.SetKind("ClusterRole")
			roleResource.SetName("manager-role")

			// Scenario: manager namespace is "user", CRD resource is "users"
			customTemplater := &HelmTemplater{
				detectedPrefix:   "test-project",
				chartName:        "test-project",
				managerNamespace: "user", // Short namespace that appears as substring
			}

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-project-manager-role
rules:
- apiGroups:
  - identity.example.com
  resources:
  - users
  - users/finalizers
  - users/status
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete`

			result := customTemplater.ApplyHelmSubstitutions(content, roleResource)

			// Critical: resource name "users" must NOT be replaced with "{{ .Release.Namespace }}s"
			Expect(result).To(ContainSubstring("- users"),
				"resource name 'users' should remain unchanged")
			Expect(result).To(ContainSubstring("- users/finalizers"),
				"resource name 'users/finalizers' should remain unchanged")
			Expect(result).To(ContainSubstring("- users/status"),
				"resource name 'users/status' should remain unchanged")

			// Ensure we didn't create templated resource names
			Expect(result).NotTo(ContainSubstring("- {{ .Release.Namespace }}s"),
				"must NOT replace 'user' substring in resource names")
			Expect(result).NotTo(MatchRegexp(`resources:\s*-\s*\{\{.*\}\}`),
				"resource names must never be templated")
		})

		It("should handle edge case where namespace is substring of multiple fields", func() {
			// Test more edge cases: namespace "app" appears in "applications", "apps", etc.
			customTemplater := &HelmTemplater{
				detectedPrefix:   "test-project",
				chartName:        "test-project",
				managerNamespace: "app",
			}

			roleBindingResource := &unstructured.Unstructured{}
			roleBindingResource.SetAPIVersion("rbac.authorization.k8s.io/v1")
			roleBindingResource.SetKind("RoleBinding")
			roleBindingResource.SetName("manager-rolebinding")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-manager-rolebinding
  namespace: app
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: app`

			result := customTemplater.ApplyHelmSubstitutions(content, roleBindingResource)

			// Namespace fields should be templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"),
				"namespace fields should be templated")

			// Verify we have exactly 2 namespace template substitutions (metadata + subject)
			namespaceTemplateCount := strings.Count(result, "namespace: {{ .Release.Namespace }}")
			Expect(namespaceTemplateCount).To(Equal(2),
				"should have exactly 2 namespace field replacements")

			// Verify apiGroup field is NOT affected (contains "app" in "rbac.authorization.k8s.io")
			Expect(result).To(ContainSubstring("apiGroup: rbac.authorization.k8s.io"),
				"apiGroup should not be affected by namespace replacement")
		})

		It("should handle ALL Kubernetes DNS patterns generically", func() {
			// This test validates DNS replacement works for ANY K8s DNS pattern
			configMapResource := &unstructured.Unstructured{}
			configMapResource.SetAPIVersion("v1")
			configMapResource.SetKind("ConfigMap")
			configMapResource.SetName("dns-config")

			content := `apiVersion: v1
kind: ConfigMap
metadata:
  name: dns-config
  namespace: test-project-system
data:
  # Standard service DNS
  service-short: api.test-project-system.svc
  service-full: api.test-project-system.svc.cluster.local
  service-port: api.test-project-system.svc:8080
  service-path: https://api.test-project-system.svc.cluster.local:443/v1
  
  # Pod DNS
  pod-dns: my-pod.test-project-system.pod.cluster.local
  
  # Endpoints DNS  
  endpoints-dns: my-service.test-project-system.endpoints.cluster.local
  
  # Headless service (StatefulSet)
  stateful-0: app-0.app-headless.test-project-system.svc.cluster.local
  stateful-1: app-1.app-headless.test-project-system.svc.cluster.local
  
  # External namespace should be preserved
  external-svc: monitoring.monitoring-system.svc.cluster.local`

			result := templater.ApplyHelmSubstitutions(content, configMapResource)

			// Verify ALL manager namespace DNS patterns are templated
			Expect(result).To(ContainSubstring("api.{{ .Release.Namespace }}.svc"))
			Expect(result).To(ContainSubstring("api.{{ .Release.Namespace }}.svc.cluster.local"))
			Expect(result).To(ContainSubstring("api.{{ .Release.Namespace }}.svc:8080"))
			Expect(result).To(ContainSubstring("api.{{ .Release.Namespace }}.svc.cluster.local:443"))
			Expect(result).To(ContainSubstring("my-pod.{{ .Release.Namespace }}.pod.cluster.local"))
			Expect(result).To(ContainSubstring("my-service.{{ .Release.Namespace }}.endpoints.cluster.local"))
			Expect(result).To(ContainSubstring("app-0.app-headless.{{ .Release.Namespace }}.svc.cluster.local"))
			Expect(result).To(ContainSubstring("app-1.app-headless.{{ .Release.Namespace }}.svc.cluster.local"))

			// Verify NO hardcoded manager namespace remains
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc"))
			Expect(result).NotTo(ContainSubstring(".test-project-system.pod"))
			Expect(result).NotTo(ContainSubstring(".test-project-system.endpoints"))

			// Verify external namespace is preserved
			Expect(result).To(ContainSubstring("monitoring.monitoring-system.svc.cluster.local"))
		})

		It("should NOT replace namespace-like strings in non-DNS contexts", func() {
			// Edge case: ensure we don't break strings that happen to contain the namespace
			configMapResource := &unstructured.Unstructured{}
			configMapResource.SetAPIVersion("v1")
			configMapResource.SetKind("ConfigMap")
			configMapResource.SetName("edge-cases")

			// Using "app" as namespace to test substring issues
			customTemplater := &HelmTemplater{
				detectedPrefix:   "test",
				chartName:        "test",
				managerNamespace: "app",
			}

			content := `apiVersion: v1
kind: ConfigMap
metadata:
  name: edge-cases
  namespace: app
data:
  # DNS patterns - should be templated
  service-url: http://api.app.svc:8080
  
  # NOT DNS patterns - should be preserved
  app-name: "my-application"
  app-version: "v1.2.3"
  mapping: "application-mapping"
  labels: "app=frontend,app.kubernetes.io/name=myapp"
  
  # Tricky: "app" in various contexts
  erapplication: "some-value"
  wrapperapp: "another-value"`

			result := customTemplater.ApplyHelmSubstitutions(content, configMapResource)

			// DNS pattern should be templated
			Expect(result).To(ContainSubstring("api.{{ .Release.Namespace }}.svc:8080"))

			// Non-DNS occurrences should be preserved
			Expect(result).To(ContainSubstring(`app-name: "my-application"`))
			Expect(result).To(ContainSubstring("app-version"))
			Expect(result).To(ContainSubstring("mapping: \"application-mapping\""))
			Expect(result).To(ContainSubstring("app=frontend"))
			Expect(result).To(ContainSubstring("app.kubernetes.io/name=myapp"))
			Expect(result).To(ContainSubstring("erapplication"))
			Expect(result).To(ContainSubstring("wrapperapp"))

			// Namespace field should be templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))
		})

		It("should handle ANY resource type with namespace references (generic test)", func() {
			// This test validates that namespace replacement is GENERIC and works
			// for any resource type, including custom resources in extras/ directory

			// Test with a custom ConfigMap (common in extras/)
			configMapResource := &unstructured.Unstructured{}
			configMapResource.SetAPIVersion("v1")
			configMapResource.SetKind("ConfigMap")
			configMapResource.SetName("custom-config")

			content := `apiVersion: v1
kind: ConfigMap
metadata:
  name: custom-config
  namespace: test-project-system
data:
  service-url: http://api-service.test-project-system.svc.cluster.local:8080
  webhook-endpoint: https://webhook.test-project-system.svc:9443/validate
  annotation-ref: "test-project-system/my-resource"`

			result := templater.ApplyHelmSubstitutions(content, configMapResource)

			// 1. Namespace field should be templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))
			Expect(result).NotTo(ContainSubstring("namespace: test-project-system"))

			// 2. DNS names in data values should be templated
			Expect(result).To(ContainSubstring("http://api-service.{{ .Release.Namespace }}.svc.cluster.local:8080"))
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc.cluster.local"))

			// 3. DNS names with ports should be templated
			Expect(result).To(ContainSubstring("https://webhook.{{ .Release.Namespace }}.svc:9443"))
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc:9443"))

			// 4. Annotation-style references should be templated
			Expect(result).To(ContainSubstring("{{ .Release.Namespace }}/my-resource"))
			Expect(result).NotTo(ContainSubstring("test-project-system/my-resource"))
		})

		It("should handle Secret with namespace references", func() {
			secretResource := &unstructured.Unstructured{}
			secretResource.SetAPIVersion("v1")
			secretResource.SetKind("Secret")
			secretResource.SetName("app-secret")

			content := `apiVersion: v1
kind: Secret
metadata:
  name: app-secret
  namespace: test-project-system
  annotations:
    source: test-project-system/config
stringData:
  database-url: postgresql://db.test-project-system.svc:5432/mydb
  redis-url: redis://cache.test-project-system.svc.cluster.local:6379`

			result := templater.ApplyHelmSubstitutions(content, secretResource)

			// Namespace field templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))

			// Annotation value templated
			Expect(result).To(ContainSubstring("source: {{ .Release.Namespace }}/config"))

			// DNS names in data templated
			Expect(result).To(ContainSubstring("postgresql://db.{{ .Release.Namespace }}.svc:5432"))
			Expect(result).To(ContainSubstring("redis://cache.{{ .Release.Namespace }}.svc.cluster.local:6379"))

			// No hardcoded namespace remains
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc"))
		})

		It("should handle Ingress with namespace references", func() {
			ingressResource := &unstructured.Unstructured{}
			ingressResource.SetAPIVersion("networking.k8s.io/v1")
			ingressResource.SetKind("Ingress")
			ingressResource.SetName("app-ingress")

			content := `apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: test-project-system
  annotations:
    nginx.ingress.kubernetes.io/auth-url: http://auth.test-project-system.svc.cluster.local/verify
    cert-manager.io/issuer: test-project-system/letsencrypt
spec:
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: app-service
            port:
              number: 80`

			result := templater.ApplyHelmSubstitutions(content, ingressResource)

			// Namespace field templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))

			// Annotation DNS templated
			Expect(result).To(ContainSubstring("http://auth.{{ .Release.Namespace }}.svc.cluster.local/verify"))

			// Annotation reference templated
			Expect(result).To(ContainSubstring("cert-manager.io/issuer: {{ .Release.Namespace }}/letsencrypt"))

			// No hardcoded namespace
			Expect(result).NotTo(ContainSubstring("test-project-system/"))
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc"))
		})

		It("should handle PodMonitor with namespace references", func() {
			podMonitorResource := &unstructured.Unstructured{}
			podMonitorResource.SetAPIVersion("monitoring.coreos.com/v1")
			podMonitorResource.SetKind("PodMonitor")
			podMonitorResource.SetName("app-monitor")

			content := `apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
  name: app-monitor
  namespace: test-project-system
spec:
  selector:
    matchLabels:
      app: myapp
  podMetricsEndpoints:
  - port: metrics
    scheme: https
    tlsConfig:
      serverName: metrics.test-project-system.svc
      ca:
        configMap:
          name: prometheus-ca
          namespace: test-project-system`

			result := templater.ApplyHelmSubstitutions(content, podMonitorResource)

			// Namespace field templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))

			// ServerName DNS templated
			Expect(result).To(ContainSubstring("serverName: metrics.{{ .Release.Namespace }}.svc"))

			// ConfigMap namespace reference templated
			namespaceCount := strings.Count(result, "namespace: {{ .Release.Namespace }}")
			Expect(namespaceCount).To(Equal(2), "both metadata and configMap namespace should be templated")

			// No hardcoded namespace in DNS
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc"))
		})

		It("should handle custom CRD with multiple namespace contexts", func() {
			customResource := &unstructured.Unstructured{}
			customResource.SetAPIVersion("example.com/v1")
			customResource.SetKind("Application")
			customResource.SetName("my-app")

			content := `apiVersion: example.com/v1
kind: Application
metadata:
  name: my-app
  namespace: test-project-system
  annotations:
    backup.velero.io/backup-volumes: test-project-system/pvc
spec:
  database:
    host: postgres.test-project-system.svc.cluster.local
    port: 5432
  messaging:
    brokerURL: amqp://rabbitmq.test-project-system.svc:5672
  externalServices:
    - name: external-api
      url: https://api.external-namespace.svc/v1
  references:
    configMapRef: test-project-system/app-config
    secretRef: test-project-system/app-secret`

			result := templater.ApplyHelmSubstitutions(content, customResource)

			// Namespace field templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))

			// Annotation reference templated
			Expect(result).To(ContainSubstring("{{ .Release.Namespace }}/pvc"))

			// DNS names templated
			Expect(result).To(ContainSubstring("postgres.{{ .Release.Namespace }}.svc.cluster.local"))
			Expect(result).To(ContainSubstring("rabbitmq.{{ .Release.Namespace }}.svc:5672"))

			// External namespace preserved (not manager namespace)
			Expect(result).To(ContainSubstring("https://api.external-namespace.svc/v1"))

			// ConfigMap/Secret refs templated
			Expect(result).To(ContainSubstring("configMapRef: {{ .Release.Namespace }}/app-config"))
			Expect(result).To(ContainSubstring("secretRef: {{ .Release.Namespace }}/app-secret"))

			// No manager namespace remains
			Expect(result).NotTo(ContainSubstring("test-project-system/"))
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc"))
		})

		It("should NOT replace namespace in non-manager context", func() {
			// Critical: cross-namespace references must be preserved
			customResource := &unstructured.Unstructured{}
			customResource.SetAPIVersion("v1")
			customResource.SetKind("ConfigMap")
			customResource.SetName("federation-config")

			content := `apiVersion: v1
kind: ConfigMap
metadata:
  name: federation-config
  namespace: test-project-system
data:
  clusters: |
    - name: cluster-a
      apiserver: https://api.cluster-a-system.svc:6443
    - name: cluster-b
      apiserver: https://api.cluster-b-system.svc:6443
  external-service: https://monitoring.monitoring-system.svc.cluster.local:9090
  internal-service: https://internal.test-project-system.svc:8080`

			result := templater.ApplyHelmSubstitutions(content, customResource)

			// Manager namespace field templated
			Expect(result).To(ContainSubstring("namespace: {{ .Release.Namespace }}"))
			Expect(result).NotTo(ContainSubstring("namespace: test-project-system"))

			// Manager namespace in DNS templated (appears once in internal-service)
			Expect(result).To(ContainSubstring("internal.{{ .Release.Namespace }}.svc:8080"))
			Expect(result).NotTo(ContainSubstring(".test-project-system.svc"))

			// External namespaces preserved (these don't match manager namespace)
			Expect(result).To(ContainSubstring("cluster-a-system.svc"))
			Expect(result).To(ContainSubstring("cluster-b-system.svc"))
			Expect(result).To(ContainSubstring("monitoring-system.svc"))
		})
	})

	Context("templatePorts", func() {
		It("should template webhook service ports", func() {
			webhookService := &unstructured.Unstructured{}
			webhookService.SetAPIVersion("v1")
			webhookService.SetKind("Service")
			webhookService.SetName("test-project-webhook-service")

			content := `apiVersion: v1
kind: Service
metadata:
  name: test-project-webhook-service
  namespace: test-project-system
spec:
  ports:
  - port: 443
    targetPort: 9443
    protocol: TCP
  selector:
    control-plane: controller-manager`

			result := templater.templatePorts(content, webhookService)

			// Should template webhook port
			Expect(result).To(ContainSubstring("targetPort: {{ .Values.webhook.port }}"))
			Expect(result).NotTo(ContainSubstring("targetPort: 9443"))
		})

		It("should template metrics service ports", func() {
			metricsService := &unstructured.Unstructured{}
			metricsService.SetAPIVersion("v1")
			metricsService.SetKind("Service")
			metricsService.SetName("test-project-controller-manager-metrics-service")

			content := `apiVersion: v1
kind: Service
metadata:
  name: test-project-controller-manager-metrics-service
  namespace: test-project-system
spec:
  ports:
  - port: 8443
    targetPort: 8443
    protocol: TCP
    name: https
  selector:
    control-plane: controller-manager`

			result := templater.templatePorts(content, metricsService)

			// Should template metrics port
			Expect(result).To(ContainSubstring("port: {{ .Values.metrics.port }}"))
			Expect(result).To(ContainSubstring("targetPort: {{ .Values.metrics.port }}"))
			Expect(result).NotTo(ContainSubstring("port: 8443"))
			Expect(result).NotTo(ContainSubstring("targetPort: 8443"))
		})

		It("should template webhook container ports in Deployment", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
spec:
  template:
    spec:
      containers:
      - name: manager
        ports:
        - containerPort: 9443
          name: webhook-server
          protocol: TCP`

			result := templater.templatePorts(content, deployment)

			// Should template webhook containerPort
			Expect(result).To(ContainSubstring("containerPort: {{ .Values.webhook.port }}"))
			Expect(result).NotTo(ContainSubstring("containerPort: 9443"))
		})

		It("should template health probe ports in Deployment", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
spec:
  template:
    spec:
      containers:
      - name: manager
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081`

			result := templater.templatePorts(content, deployment)

			Expect(result).To(ContainSubstring("port: 8081"))
			Expect(result).NotTo(ContainSubstring("{{ .Values"))
		})

		It("should template port-related args in Deployment", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
spec:
  template:
    spec:
      containers:
      - name: manager
        args:
        - --metrics-bind-address=:8443
        - --health-probe-bind-address=:8081
        - --leader-elect`

			result := templater.templatePorts(content, deployment)

			Expect(result).To(ContainSubstring("--metrics-bind-address=:{{ .Values.metrics.port }}"))
			Expect(result).NotTo(ContainSubstring("--metrics-bind-address=:8443"))
			Expect(result).To(ContainSubstring("--health-probe-bind-address=:8081"))
			Expect(result).To(ContainSubstring("--leader-elect"))
		})

		It("should template custom port values", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
spec:
  template:
    spec:
      containers:
      - name: manager
        args:
        - --metrics-bind-address=:9090
        - --health-probe-bind-address=:9091
        - --webhook-port=9444
        ports:
        - containerPort: 9444
          name: webhook-server
        livenessProbe:
          httpGet:
            port: 9091`

			result := templater.templatePorts(content, deployment)

			Expect(result).To(ContainSubstring("--metrics-bind-address=:{{ .Values.metrics.port }}"))
			Expect(result).To(ContainSubstring("--webhook-port={{ .Values.webhook.port }}"))
			Expect(result).To(ContainSubstring("containerPort: {{ .Values.webhook.port }}"))
			Expect(result).To(ContainSubstring("--health-probe-bind-address=:9091"))
			Expect(result).To(ContainSubstring("port: 9091"))
		})

		It("should not template non-webhook/metrics resources", func() {
			regularService := &unstructured.Unstructured{}
			regularService.SetAPIVersion("v1")
			regularService.SetKind("Service")
			regularService.SetName("test-project-some-other-service")

			content := `apiVersion: v1
kind: Service
metadata:
  name: test-project-some-other-service
spec:
  ports:
  - port: 8080
    targetPort: 8080`

			result := templater.templatePorts(content, regularService)

			// Should not template regular service ports
			Expect(result).To(ContainSubstring("port: 8080"))
			Expect(result).To(ContainSubstring("targetPort: 8080"))
			Expect(result).NotTo(ContainSubstring("{{ .Values"))
		})
	})

	Context("cert-manager resource name templating", func() {
		It("should template Certificate resource name with chart.fullname", func() {
			cert := &unstructured.Unstructured{}
			cert.SetAPIVersion("cert-manager.io/v1")
			cert.SetKind("Certificate")
			cert.SetName("test-project-serving-cert")

			content := `apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: test-project-serving-cert
  namespace: test-project-system`

			result := templater.ApplyHelmSubstitutions(content, cert)

			expectedCert := `name: {{ include "test-project.resourceName" (dict "suffix" "serving-cert" "context" $) }}`
			Expect(result).To(ContainSubstring(expectedCert))
			Expect(result).NotTo(ContainSubstring("name: test-project-serving-cert"))
		})

		It("should template Issuer resource name with chart.fullname", func() {
			issuer := &unstructured.Unstructured{}
			issuer.SetAPIVersion("cert-manager.io/v1")
			issuer.SetKind("Issuer")
			issuer.SetName("test-project-selfsigned-issuer")

			content := `apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: test-project-selfsigned-issuer
  namespace: test-project-system
spec:
  selfSigned: {}`

			result := templater.ApplyHelmSubstitutions(content, issuer)

			Expect(result).To(ContainSubstring(expectedIssuerName))
			Expect(result).NotTo(ContainSubstring("name: test-project-selfsigned-issuer"))
		})

		It("should template issuer reference in certificates with chart.fullname", func() {
			cert := &unstructured.Unstructured{}
			cert.SetAPIVersion("cert-manager.io/v1")
			cert.SetKind("Certificate")
			cert.SetName("test-project-serving-cert")

			content := `apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: test-project-serving-cert
spec:
  issuerRef:
    kind: Issuer
    name: test-project-selfsigned-issuer`

			result := templater.ApplyHelmSubstitutions(content, cert)

			Expect(result).To(ContainSubstring(expectedIssuerName))
			Expect(result).NotTo(ContainSubstring("name: test-project-selfsigned-issuer"))
		})

		It("should template all resource types generically", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  template:
    spec:
      serviceAccountName: test-project-controller-manager`

			result := templater.ApplyHelmSubstitutions(content, deployment)

			// All name fields should use test-project.resourceName
			expectedName := `name: {{ include "test-project.resourceName" (dict "suffix" "controller-manager" "context" $) }}`
			expectedSA := `serviceAccountName: {{ include "test-project.resourceName" ` +
				`(dict "suffix" "controller-manager" "context" $) }}`
			Expect(result).To(ContainSubstring(expectedName))
			Expect(result).To(ContainSubstring(expectedSA))
			Expect(result).NotTo(ContainSubstring("name: test-project-controller-manager"))
		})

		It("should handle custom kustomize prefix", func() {
			customPrefixTemplater := &HelmTemplater{
				detectedPrefix:   "ln",           // Custom short prefix from kustomize
				chartName:        "test-project", // Chart/project name
				managerNamespace: "ln-system",    // Manager namespace
			}

			issuer := &unstructured.Unstructured{}
			issuer.SetAPIVersion("cert-manager.io/v1")
			issuer.SetKind("Issuer")
			issuer.SetName("ln-selfsigned-issuer")

			content := `apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: ln-selfsigned-issuer
  labels:
    app.kubernetes.io/name: ln`

			result := customPrefixTemplater.ApplyHelmSubstitutions(content, issuer)

			// Resource name uses test-project.resourceName
			Expect(result).To(ContainSubstring(expectedIssuerName))
			Expect(result).NotTo(ContainSubstring("name: ln-selfsigned-issuer"))
			// Label uses test-project.name
			Expect(result).To(ContainSubstring("app.kubernetes.io/name: {{ include \"test-project.name\" . }}"))
			Expect(result).NotTo(ContainSubstring("app.kubernetes.io/name: ln"))
		})

		It("should template RoleBinding roleRef and subjects", func() {
			rb := &unstructured.Unstructured{}
			rb.SetAPIVersion("rbac.authorization.k8s.io/v1")
			rb.SetKind("RoleBinding")
			rb.SetName("test-project-leader-election-rolebinding")

			content := `apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-leader-election-rolebinding
roleRef:
  name: test-project-leader-election-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager`

			result := templater.ApplyHelmSubstitutions(content, rb)

			// All references should use test-project.resourceName
			expectedRB := `name: {{ include "test-project.resourceName" ` +
				`(dict "suffix" "leader-election-rolebinding" "context" $) }}`
			expectedRole := `name: {{ include "test-project.resourceName" ` +
				`(dict "suffix" "leader-election-role" "context" $) }}`
			expectedSA := `name: {{ include "test-project.resourceName" ` +
				`(dict "suffix" "controller-manager" "context" $) }}`
			Expect(result).To(ContainSubstring(expectedRB))
			Expect(result).To(ContainSubstring(expectedRole))
			Expect(result).To(ContainSubstring(expectedSA))
		})
	})

	Context("custom container name support", func() {
		It("should template deployment fields when container name is not 'manager'", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			// Deployment with custom container name "controller-test" using default-container annotation
			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
spec:
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: controller-test
    spec:
      containers:
      - name: controller-test
        image: controller:latest
        imagePullPolicy: Always
        env:
        - name: POD_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        args:
        - --leader-elect
        - --health-probe-bind-address=:8081
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
        volumeMounts: []
      serviceAccountName: controller-manager
      volumes: []`

			result := templater.ApplyHelmSubstitutions(content, deployment)

			// Should template image reference (not hardcoded)
			Expect(result).To(ContainSubstring(
				`image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}"`))
			Expect(result).NotTo(ContainSubstring("image: controller:latest"))

			// Should template imagePullPolicy
			Expect(result).To(ContainSubstring("imagePullPolicy: {{ .Values.manager.image.pullPolicy }}"))
			Expect(result).NotTo(ContainSubstring("imagePullPolicy: Always"))

			// Should template resources
			Expect(result).To(ContainSubstring("{{- if .Values.manager.resources }}"))
			Expect(result).To(ContainSubstring("{{- toYaml .Values.manager.resources | nindent"))

			// Env list + envOverrides (--set). Secret refs go in env list.
			Expect(result).To(ContainSubstring(".Values.manager.env"))
			Expect(result).To(ContainSubstring("toYaml .Values.manager.env"))
			Expect(result).To(ContainSubstring("envOverrides"))

			// Should template args
			Expect(result).To(ContainSubstring("{{- range .Values.manager.args }}"))

			// Container name should remain "controller-test"
			Expect(result).To(ContainSubstring("name: controller-test"))
		})

		It("should fall back to 'manager' when default-container annotation is missing", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			// Deployment without default-container annotation (backward compatibility test)
			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
spec:
  template:
    spec:
      containers:
      - name: manager
        image: controller:latest
        resources:
          limits:
            cpu: 500m
            memory: 128Mi`

			result := templater.ApplyHelmSubstitutions(content, deployment)

			// Should still template fields for "manager" container
			Expect(result).To(ContainSubstring(
				`image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}"`))
			Expect(result).To(ContainSubstring("{{- if .Values.manager.resources }}"))
		})

		It("should not template when container name doesn't match annotation", func() {
			deployment := &unstructured.Unstructured{}
			deployment.SetAPIVersion("apps/v1")
			deployment.SetKind("Deployment")
			deployment.SetName("test-project-controller-manager")

			// Deployment with mismatched annotation and container name
			content := `apiVersion: apps/v1
kind: Deployment
metadata:
  name: test-project-controller-manager
spec:
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: main-container
    spec:
      containers:
      - name: sidecar
        image: sidecar:latest
        resources:
          limits:
            cpu: 100m`

			result := templater.ApplyHelmSubstitutions(content, deployment)

			// Should NOT template sidecar container (doesn't match annotation)
			Expect(result).To(ContainSubstring("image: sidecar:latest"))
			Expect(result).NotTo(ContainSubstring("{{ .Values.manager.image.repository }}"))
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/parser.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"fmt"
	"io"
	"os"
	"strings"

	"go.yaml.in/yaml/v3"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// ParsedResources holds Kubernetes resources organized by type for Helm chart generation
type ParsedResources struct {
	// Core Kubernetes resources
	Namespace  *unstructured.Unstructured
	Deployment *unstructured.Unstructured
	Services   []*unstructured.Unstructured

	// RBAC resources
	ServiceAccount      *unstructured.Unstructured
	Roles               []*unstructured.Unstructured
	ClusterRoles        []*unstructured.Unstructured
	RoleBindings        []*unstructured.Unstructured
	ClusterRoleBindings []*unstructured.Unstructured

	// CRD and API resources
	CustomResourceDefinitions []*unstructured.Unstructured
	WebhookConfigurations     []*unstructured.Unstructured

	// Custom Resource instances (samples) - instances of the CRDs defined in this project
	// These should go to samples/ directory for manual post-install, not be installed by Helm
	CustomResources []*unstructured.Unstructured

	// Cert-manager resources
	Certificates []*unstructured.Unstructured
	Issuer       *unstructured.Unstructured

	// Monitoring resources
	ServiceMonitors []*unstructured.Unstructured

	// Other resources not fitting above categories
	Other []*unstructured.Unstructured
}

// Parser parses kustomize output and extracts resources by type
type Parser struct {
	filePath string
}

// NewParser creates a new parser for the given kustomize output file
func NewParser(filePath string) *Parser {
	return &Parser{filePath: filePath}
}

// Parse reads and parses the kustomize output file into organized resource groups
func (p *Parser) Parse() (*ParsedResources, error) {
	file, err := os.Open(p.filePath)
	if err != nil {
		return nil, fmt.Errorf("failed to open file %s: %w", p.filePath, err)
	}
	defer func() {
		_ = file.Close()
	}()

	return p.ParseFromReader(file)
}

// ParseFromReader parses multi-document YAML from a reader and categorizes resources by type
func (p *Parser) ParseFromReader(reader io.Reader) (*ParsedResources, error) {
	decoder := yaml.NewDecoder(reader)
	resources := &ParsedResources{
		CustomResourceDefinitions: make([]*unstructured.Unstructured, 0),
		Roles:                     make([]*unstructured.Unstructured, 0),
		ClusterRoles:              make([]*unstructured.Unstructured, 0),
		RoleBindings:              make([]*unstructured.Unstructured, 0),
		ClusterRoleBindings:       make([]*unstructured.Unstructured, 0),
		Services:                  make([]*unstructured.Unstructured, 0),
		Certificates:              make([]*unstructured.Unstructured, 0),
		WebhookConfigurations:     make([]*unstructured.Unstructured, 0),
		ServiceMonitors:           make([]*unstructured.Unstructured, 0),
		CustomResources:           make([]*unstructured.Unstructured, 0),
		Other:                     make([]*unstructured.Unstructured, 0),
	}

	for {
		var doc map[string]any
		err := decoder.Decode(&doc)
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("failed to decode YAML document: %w", err)
		}

		// Skip empty documents
		if doc == nil {
			continue
		}

		obj := &unstructured.Unstructured{Object: doc}
		p.categorizeResource(obj, resources)
	}

	// After parsing all resources, identify Custom Resources by matching against CRD API groups
	p.identifyCustomResources(resources)

	return resources, nil
}

// categorizeResource sorts a Kubernetes resource into the appropriate category based on kind and API version
func (p *Parser) categorizeResource(obj *unstructured.Unstructured, resources *ParsedResources) {
	kind := obj.GetKind()
	apiVersion := obj.GetAPIVersion()

	switch {
	case kind == "Namespace":
		resources.Namespace = obj
	case kind == "CustomResourceDefinition":
		resources.CustomResourceDefinitions = append(resources.CustomResourceDefinitions, obj)
	case kind == "ServiceAccount":
		resources.ServiceAccount = obj
	case kind == "Role":
		resources.Roles = append(resources.Roles, obj)
	case kind == "ClusterRole":
		resources.ClusterRoles = append(resources.ClusterRoles, obj)
	case kind == "RoleBinding":
		resources.RoleBindings = append(resources.RoleBindings, obj)
	case kind == "ClusterRoleBinding":
		resources.ClusterRoleBindings = append(resources.ClusterRoleBindings, obj)
	case kind == "Service":
		resources.Services = append(resources.Services, obj)
	case kind == "Deployment":
		resources.Deployment = obj
	case kind == "Certificate" && apiVersion == "cert-manager.io/v1":
		resources.Certificates = append(resources.Certificates, obj)
	case kind == "Issuer" && apiVersion == "cert-manager.io/v1":
		resources.Issuer = obj
	case kind == "ValidatingWebhookConfiguration" || kind == "MutatingWebhookConfiguration":
		resources.WebhookConfigurations = append(resources.WebhookConfigurations, obj)
	case kind == "ServiceMonitor" && apiVersion == "monitoring.coreos.com/v1":
		resources.ServiceMonitors = append(resources.ServiceMonitors, obj)
	default:
		resources.Other = append(resources.Other, obj)
	}
}

// identifyCustomResources moves resources from Other to CustomResources if they are instances of project CRDs
func (p *Parser) identifyCustomResources(resources *ParsedResources) {
	// Build a set of API groups from the CRDs defined in this project
	crdAPIGroups := make(map[string]bool)
	for _, crd := range resources.CustomResourceDefinitions {
		// Extract the group from the CRD spec
		group, found, err := unstructured.NestedString(crd.Object, "spec", "group")
		if found && err == nil && group != "" {
			crdAPIGroups[group] = true
		}
	}

	// If no CRDs found, nothing to do
	if len(crdAPIGroups) == 0 {
		return
	}

	// Separate Custom Resources from Other resources
	var remainingOther []*unstructured.Unstructured
	for _, resource := range resources.Other {
		if resource == nil {
			continue
		}

		// Extract API group from the resource's apiVersion (format: group/version or just version)
		apiVersion := resource.GetAPIVersion()
		apiGroup := extractAPIGroup(apiVersion)

		// If this resource's API group matches one of our CRDs, it's a Custom Resource
		if crdAPIGroups[apiGroup] {
			resources.CustomResources = append(resources.CustomResources, resource)
		} else {
			remainingOther = append(remainingOther, resource)
		}
	}

	resources.Other = remainingOther
}

// GetIgnoredCustomResources returns the list of Custom Resource instances that will be ignored
func (pr *ParsedResources) GetIgnoredCustomResources() []*unstructured.Unstructured {
	return pr.CustomResources
}

// extractAPIGroup extracts the group from an apiVersion string
// Examples: "batch.tutorial.kubebuilder.io/v1" -> "batch.tutorial.kubebuilder.io"
//
//	"apps/v1" -> "apps"
//	"v1" -> "" (core API group)
func extractAPIGroup(apiVersion string) string {
	parts := strings.Split(apiVersion, "/")
	if len(parts) == 2 {
		return parts[0]
	}
	return "" // Core API group (v1)
}

func (pr *ParsedResources) EstimatePrefix(projectName string) string {
	prefix := projectName
	if pr.Deployment != nil {
		if name := pr.Deployment.GetName(); name != "" {
			deploymentPrefix, found := strings.CutSuffix(name, "-controller-manager")
			if found {
				prefix = deploymentPrefix
			}
		}
	}
	// Double check that the prefix is also the prefix for the service names
	for _, svc := range pr.Services {
		if name := svc.GetName(); name != "" {
			if !strings.HasPrefix(name, prefix) {
				// If not, fallback to just project name
				prefix = projectName
				break
			}
		}
	}
	return prefix
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/parser_test.go
================================================
//go:build integration

/*
Copyright 2025 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 kustomize

import (
	"os"
	"path/filepath"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Parser", func() {
	var (
		parser   *Parser
		tempFile string
	)

	BeforeEach(func() {
		// Create a temporary file for testing
		tempDir := GinkgoT().TempDir()
		tempFile = filepath.Join(tempDir, "test-manifest.yaml")
	})

	Context("with valid YAML containing various resources", func() {
		BeforeEach(func() {
			yamlContent := `---
apiVersion: v1
kind: Namespace
metadata:
  name: test-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: controller-manager
  namespace: test-system
spec:
  replicas: 1
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: controller-manager
  namespace: test-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: manager-role
rules:
- apiGroups: [""]
  resources: ["*"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: manager-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: test-system
---
apiVersion: v1
kind: Service
metadata:
  name: controller-manager-metrics-service
  namespace: test-system
spec:
  ports:
  - name: https
    port: 8443
    targetPort: 8443
  selector:
    control-plane: controller-manager
`
			err := os.WriteFile(tempFile, []byte(yamlContent), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
		})

		It("should parse all resources correctly", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())
			Expect(resources).NotTo(BeNil())

			// Check that resources were parsed
			Expect(resources.Namespace).NotTo(BeNil())
			Expect(resources.Deployment).NotTo(BeNil())
			Expect(resources.ServiceAccount).NotTo(BeNil())

			// Check RBAC resources
			Expect(resources.ClusterRoles).To(HaveLen(1))
			Expect(resources.ClusterRoleBindings).To(HaveLen(1))

			// Check Services
			Expect(resources.Services).To(HaveLen(1))
		})

		It("should identify correct resource types", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			Expect(resources.Namespace.GetKind()).To(Equal("Namespace"))
			Expect(resources.Deployment.GetKind()).To(Equal("Deployment"))
			Expect(resources.ServiceAccount.GetKind()).To(Equal("ServiceAccount"))
		})
	})

	Context("with webhook configuration", func() {
		BeforeEach(func() {
			yamlContent := `---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-webhook-configuration
webhooks:
- name: test.example.com
  clientConfig:
    service:
      name: webhook-service
      namespace: test-system
      path: "/validate"
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: serving-cert
  namespace: test-system
spec:
  dnsNames:
  - webhook-service.test-system.svc
  - webhook-service.test-system.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: webhook-server-cert
`
			err := os.WriteFile(tempFile, []byte(yamlContent), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
		})

		It("should parse webhook configurations", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			Expect(resources.WebhookConfigurations).To(HaveLen(1))
			Expect(resources.Certificates).To(HaveLen(1))

			webhook := resources.WebhookConfigurations[0]
			Expect(webhook.GetKind()).To(Equal("ValidatingWebhookConfiguration"))

			cert := resources.Certificates[0]
			Expect(cert.GetKind()).To(Equal("Certificate"))
		})
	})

	Context("with ServiceMonitor", func() {
		BeforeEach(func() {
			yamlContent := `---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: controller-manager-metrics-monitor
  namespace: test-system
spec:
  endpoints:
  - path: /metrics
    port: https
  selector:
    matchLabels:
      control-plane: controller-manager
`
			err := os.WriteFile(tempFile, []byte(yamlContent), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
		})

		It("should parse ServiceMonitor", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			Expect(resources.ServiceMonitors).To(HaveLen(1))

			monitor := resources.ServiceMonitors[0]
			Expect(monitor.GetKind()).To(Equal("ServiceMonitor"))
		})
	})

	Context("with empty or invalid YAML", func() {
		It("should handle empty file gracefully", func() {
			err := os.WriteFile(tempFile, []byte(""), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())
			Expect(resources).NotTo(BeNil())
		})

		It("should return error for invalid YAML", func() {
			invalidYAML := `invalid: yaml: content: [unclosed`
			err := os.WriteFile(tempFile, []byte(invalidYAML), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
			_, err = parser.Parse()
			Expect(err).To(HaveOccurred())
		})

		It("should return error for non-existent file", func() {
			parser = NewParser("/non/existent/file.yaml")
			_, err := parser.Parse()
			Expect(err).To(HaveOccurred())
		})
	})

	Context("resource organization", func() {
		BeforeEach(func() {
			yamlContent := `---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: tests.example.com
spec:
  group: example.com
  versions:
  - name: v1
    served: true
    storage: true
    schema:
      openAPIV3Schema:
        type: object
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: selfsigned-issuer
  namespace: test-system
spec:
  selfSigned: {}
`
			err := os.WriteFile(tempFile, []byte(yamlContent), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
		})

		It("should organize CRDs and Issuers correctly", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			Expect(resources.CustomResourceDefinitions).To(HaveLen(1))
			Expect(resources.Issuer).NotTo(BeNil())

			crd := resources.CustomResourceDefinitions[0]
			Expect(crd.GetKind()).To(Equal("CustomResourceDefinition"))

			issuer := resources.Issuer
			Expect(issuer.GetKind()).To(Equal("Issuer"))
		})
	})

	Context("with custom prefix", func() {
		BeforeEach(func() {
			yamlContent := `---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ln-controller-manager
  namespace: long-name-test-system
spec:
  replicas: 1
---
apiVersion: v1
kind: Service
metadata:
  name: ln-controller-manager-metrics-service
  namespace: long-name-test-system
spec:
  ports:
  - name: https
    port: 8443
    targetPort: 8443
  selector:
    control-plane: ln-controller-manager
`
			err := os.WriteFile(tempFile, []byte(yamlContent), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
		})

		It("should use the correct prefix", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			Expect(resources.EstimatePrefix("long-name")).To(Equal("ln"))
		})
	})

	Context("with multiple namespace-scoped Roles", func() {
		BeforeEach(func() {
			// This test validates the parser correctly handles multiple Roles
			// with explicit namespaces (for cross-namespace permissions, leader election, etc.)
			yamlContent := `---
apiVersion: v1
kind: Namespace
metadata:
  name: test-project-system
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: test-project-manager-role
rules:
- apiGroups: ["example.com"]
  resources: ["myresources"]
  verbs: ["get", "list", "watch", "create", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-manager-role
  namespace: infrastructure
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "patch", "update", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-leader-election-role
  namespace: production
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["events"]
  verbs: ["create", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: test-project-leader-election-role
  namespace: test-project-system
rules:
- apiGroups: ["coordination.k8s.io"]
  resources: ["leases"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: test-project-manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-manager-rolebinding
  namespace: infrastructure
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-manager-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-leader-election-rolebinding
  namespace: production
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-leader-election-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: test-project-leader-election-rolebinding
  namespace: test-project-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: test-project-leader-election-role
subjects:
- kind: ServiceAccount
  name: test-project-controller-manager
  namespace: test-project-system
`
			err := os.WriteFile(tempFile, []byte(yamlContent), 0o600)
			Expect(err).NotTo(HaveOccurred())

			parser = NewParser(tempFile)
		})

		It("should parse all Roles including those with explicit namespaces", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())
			Expect(resources).NotTo(BeNil())

			// Should parse all 3 namespace-scoped Roles
			Expect(resources.Roles).To(HaveLen(3), "should have 3 namespace-scoped Roles")

			// Should also parse the ClusterRole
			Expect(resources.ClusterRoles).To(HaveLen(1), "should have 1 ClusterRole")

			// Verify each Role has correct kind
			for _, role := range resources.Roles {
				Expect(role.GetKind()).To(Equal("Role"))
			}

			// Verify namespaces are preserved in the parsed objects
			namespaces := make(map[string]bool)
			for _, role := range resources.Roles {
				ns := role.GetNamespace()
				Expect(ns).NotTo(BeEmpty(), "Role should have namespace")
				namespaces[ns] = true
			}

			// Should have Roles in 3 different namespaces
			Expect(namespaces).To(HaveLen(3), "should have Roles in 3 different namespaces")
			Expect(namespaces).To(HaveKey("infrastructure"))
			Expect(namespaces).To(HaveKey("production"))
			Expect(namespaces).To(HaveKey("test-project-system"))
		})

		It("should parse all RoleBindings including those with explicit namespaces", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			// Should parse all 3 namespace-scoped RoleBindings
			Expect(resources.RoleBindings).To(HaveLen(3), "should have 3 RoleBindings")

			// Should also parse the ClusterRoleBinding
			Expect(resources.ClusterRoleBindings).To(HaveLen(1), "should have 1 ClusterRoleBinding")

			// Verify namespaces are preserved
			namespaces := make(map[string]bool)
			for _, rb := range resources.RoleBindings {
				ns := rb.GetNamespace()
				Expect(ns).NotTo(BeEmpty(), "RoleBinding should have namespace")
				namespaces[ns] = true
			}

			Expect(namespaces).To(HaveLen(3), "should have RoleBindings in 3 different namespaces")
		})

		It("should correctly categorize RBAC resources separately from other resources", func() {
			resources, err := parser.Parse()
			Expect(err).NotTo(HaveOccurred())

			// RBAC resources should be in their specific fields
			Expect(resources.ServiceAccount).NotTo(BeNil())
			Expect(resources.ClusterRoles).To(HaveLen(1))
			Expect(resources.Roles).To(HaveLen(3))
			Expect(resources.ClusterRoleBindings).To(HaveLen(1))
			Expect(resources.RoleBindings).To(HaveLen(3))

			// RBAC resources should NOT be in "Other"
			Expect(resources.Other).To(BeEmpty(), "RBAC resources should not be in Other category")
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/resource_organizer.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"strings"

	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// ResourceOrganizer groups Kubernetes resources by their logical function
type ResourceOrganizer struct {
	resources *ParsedResources
}

// NewResourceOrganizer creates a new resource organizer
func NewResourceOrganizer(resources *ParsedResources) *ResourceOrganizer {
	return &ResourceOrganizer{
		resources: resources,
	}
}

// OrganizeByFunction groups resources by their logical function matching config/ directory structure
func (o *ResourceOrganizer) OrganizeByFunction() map[string][]*unstructured.Unstructured {
	groups := make(map[string][]*unstructured.Unstructured)

	// CRDs - Custom Resource Definitions
	if len(o.resources.CustomResourceDefinitions) > 0 {
		groups["crd"] = o.resources.CustomResourceDefinitions
	}

	// RBAC - Role-Based Access Control resources
	rbacResources := o.collectRBACResources()
	if len(rbacResources) > 0 {
		groups["rbac"] = rbacResources
	}

	// Manager - Deployment and related resources
	if o.resources.Deployment != nil {
		groups["manager"] = []*unstructured.Unstructured{o.resources.Deployment}
	}

	// Metrics - Metrics services and related resources
	metricsResources := o.collectMetricsResources()
	if len(metricsResources) > 0 {
		groups["metrics"] = metricsResources
	}

	// Webhook - Webhook configurations and webhook services
	webhookResources := o.collectWebhookResources()
	if len(webhookResources) > 0 {
		groups["webhook"] = webhookResources
	}

	// Cert-manager - Certificate issuers and related resources
	certManagerResources := o.collectCertManagerResources()
	if len(certManagerResources) > 0 {
		groups["cert-manager"] = certManagerResources
	}

	// Prometheus - Prometheus ServiceMonitors and monitoring resources
	prometheusResources := o.collectPrometheusResources()
	if len(prometheusResources) > 0 {
		groups["prometheus"] = prometheusResources
	}

	// Extras - Uncategorized resources (services, configmaps, secrets, etc. not fitting above categories)
	// This includes both uncategorized services and all resources from the "Other" category
	extrasResources := o.collectExtrasResources()
	if len(extrasResources) > 0 {
		groups["extras"] = extrasResources
	}

	return groups
}

// collectRBACResources gathers all RBAC-related resources
func (o *ResourceOrganizer) collectRBACResources() []*unstructured.Unstructured {
	var rbacResources []*unstructured.Unstructured

	// Service account
	if o.resources.ServiceAccount != nil {
		rbacResources = append(rbacResources, o.resources.ServiceAccount)
	}

	// Roles and bindings
	rbacResources = append(rbacResources, o.resources.Roles...)
	rbacResources = append(rbacResources, o.resources.ClusterRoles...)
	rbacResources = append(rbacResources, o.resources.RoleBindings...)
	rbacResources = append(rbacResources, o.resources.ClusterRoleBindings...)

	return rbacResources
}

// collectWebhookResources gathers webhook-related resources
func (o *ResourceOrganizer) collectWebhookResources() []*unstructured.Unstructured {
	var webhookResources []*unstructured.Unstructured

	// Webhook configurations (ValidatingWebhookConfiguration, MutatingWebhookConfiguration)
	webhookResources = append(webhookResources, o.resources.WebhookConfigurations...)

	// Webhook services (services containing "webhook" in the name)
	for _, service := range o.resources.Services {
		if o.isWebhookService(service) {
			webhookResources = append(webhookResources, service)
		}
	}

	return webhookResources
}

// collectCertManagerResources gathers cert-manager related resources
func (o *ResourceOrganizer) collectCertManagerResources() []*unstructured.Unstructured {
	var certManagerResources []*unstructured.Unstructured

	// Certificate issuers
	if o.resources.Issuer != nil {
		certManagerResources = append(certManagerResources, o.resources.Issuer)
	}

	// Certificates (both webhook and metrics certificates are cert-manager resources)
	certManagerResources = append(certManagerResources, o.resources.Certificates...)

	return certManagerResources
}

// collectMetricsResources gathers metrics-related resources
func (o *ResourceOrganizer) collectMetricsResources() []*unstructured.Unstructured {
	var metricsResources []*unstructured.Unstructured

	// Metrics services (services containing "metrics" in the name)
	for _, service := range o.resources.Services {
		if o.isMetricsService(service) {
			metricsResources = append(metricsResources, service)
		}
	}

	return metricsResources
}

// collectPrometheusResources gathers prometheus related resources
func (o *ResourceOrganizer) collectPrometheusResources() []*unstructured.Unstructured {
	prometheusResources := make([]*unstructured.Unstructured, 0, len(o.resources.ServiceMonitors))

	// ServiceMonitors
	prometheusResources = append(prometheusResources, o.resources.ServiceMonitors...)

	return prometheusResources
}

// isWebhookService determines if a service is webhook-related
// Verifies KIND is "Service" and name ends with "webhook-service" suffix
func (o *ResourceOrganizer) isWebhookService(service *unstructured.Unstructured) bool {
	// Only match resources with KIND "Service" (excludes ServiceAccount, ServiceMonitor, etc.)
	if service.GetKind() != kindService {
		return false
	}
	serviceName := service.GetName()
	// Use suffix matching to avoid false positives when project names contain "webhook"
	// e.g., project "test-helm-no-webhooks" should not match webhook services
	return strings.HasSuffix(serviceName, "webhook-service")
}

// isMetricsService determines if a service is metrics-related
// Verifies KIND is "Service" and name ends with "metrics-service" suffix
func (o *ResourceOrganizer) isMetricsService(service *unstructured.Unstructured) bool {
	// Only match resources with KIND "Service" (excludes ServiceAccount, ServiceMonitor, etc.)
	if service.GetKind() != kindService {
		return false
	}
	serviceName := service.GetName()
	// Use suffix matching to avoid false positives when project names contain "metrics"
	return strings.HasSuffix(serviceName, "metrics-service")
}

// collectExtrasResources gathers uncategorized resources that don't fit standard categories
func (o *ResourceOrganizer) collectExtrasResources() []*unstructured.Unstructured {
	var extrasResources []*unstructured.Unstructured

	// Collect services that are neither webhook nor metrics services
	for _, service := range o.resources.Services {
		if !o.isWebhookService(service) && !o.isMetricsService(service) {
			extrasResources = append(extrasResources, service)
		}
	}

	// Collect all other uncategorized resources (ConfigMaps, Secrets, etc.)
	extrasResources = append(extrasResources, o.resources.Other...)

	return extrasResources
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/kustomize/suite_test.go
================================================
/*
Copyright 2025 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 kustomize

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestKustomize(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Kustomize Suite")
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart-templates/consts.go
================================================
/*
Copyright 2026 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 charttemplates

const defaultOutputDir = "dist"


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart-templates/helpers_tpl.go
================================================
/*
Copyright 2025 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 charttemplates

import (
	"fmt"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmHelpers{}

// HelmHelpers scaffolds the _helpers.tpl file for Helm charts
type HelmHelpers struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// OutputDir specifies the output directory for the chart
	OutputDir string
	// Force if true allows overwriting the scaffolded file
	Force bool
}

// SetTemplateDefaults sets the default template configuration
func (f *HelmHelpers) SetTemplateDefaults() error {
	if f.Path == "" {
		outputDir := f.OutputDir
		if outputDir == "" {
			outputDir = "dist"
		}
		f.Path = filepath.Join(outputDir, "chart", "templates", "_helpers.tpl")
	}

	f.TemplateBody = f.generateHelpersTemplate()

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

// generateHelpersTemplate creates the _helpers.tpl content with project-specific template names
func (f *HelmHelpers) generateHelpersTemplate() string {
	// Use project name as prefix (e.g., "project-v4-with-plugins")
	// This creates templates like "project-v4-with-plugins.name" instead of generic "chart.name"
	// preventing collisions when chart is used as a Helm dependency
	prefix := f.ProjectName

	return fmt.Sprintf(helmHelpersTemplate, prefix, prefix, prefix, prefix, prefix)
}

const helmHelpersTemplate = `{{` + "`" + `{{/*
Expand the name of the chart.
*/}}` + "`" + `}}
{{` + "`" + `{{- define "%s.name" -}}` + "`" + `}}
{{` + "`" + `{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}

{{` + "`" + `{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}` + "`" + `}}
{{` + "`" + `{{- define "%s.fullname" -}}` + "`" + `}}
{{` + "`" + `{{- if .Values.fullnameOverride }}` + "`" + `}}
{{` + "`" + `{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}` + "`" + `}}
{{` + "`" + `{{- else }}` + "`" + `}}
{{` + "`" + `{{- $name := default .Chart.Name .Values.nameOverride }}` + "`" + `}}
{{` + "`" + `{{- if contains $name .Release.Name }}` + "`" + `}}
{{` + "`" + `{{- .Release.Name | trunc 63 | trimSuffix "-" }}` + "`" + `}}
{{` + "`" + `{{- else }}` + "`" + `}}
{{` + "`" + `{{- printf "%%s-%%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}

{{` + "`" + `{{/*
Namespace for generated references.
Always uses the Helm release namespace.
*/}}` + "`" + `}}
{{` + "`" + `{{- define "%s.namespaceName" -}}` + "`" + `}}
{{` + "`" + `{{- .Release.Namespace }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}

{{` + "`" + `{{/*
Resource name with proper truncation for Kubernetes 63-character limit.
Takes a dict with:
  - .suffix: Resource name suffix (e.g., "metrics", "webhook")
  - .context: Template context (root context with .Values, .Release, etc.)
Dynamically calculates safe truncation to ensure total name length <= 63 chars.
*/}}` + "`" + `}}
{{` + "`" + `{{- define "%s.resourceName" -}}` + "`" + `}}
{{` + "`" + `{{- $fullname := include "%s.fullname" .context }}` + "`" + `}}
{{` + "`" + `{{- $suffix := .suffix }}` + "`" + `}}
{{` + "`" + `{{- $maxLen := sub 62 (len $suffix) | int }}` + "`" + `}}
{{` + "`" + `{{- if gt (len $fullname) $maxLen }}` + "`" + `}}
{{` + "`" + `{{- printf "%%s-%%s" (trunc $maxLen $fullname | trimSuffix "-") $suffix ` +
	`| trunc 63 | trimSuffix "-" }}` + "`" + `}}
{{` + "`" + `{{- else }}` + "`" + `}}
{{` + "`" + `{{- printf "%%s-%%s" $fullname $suffix | trunc 63 | trimSuffix "-" }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
{{` + "`" + `{{- end }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart-templates/notes.go
================================================
/*
Copyright 2025 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 charttemplates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &Notes{}

// Notes scaffolds the NOTES.txt file for Helm charts
type Notes struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// OutputDir specifies the output directory for the chart
	OutputDir string
	// Force if true allows overwriting the scaffolded file
	Force bool
}

// SetTemplateDefaults sets the default template configuration
func (f *Notes) SetTemplateDefaults() error {
	if f.Path == "" {
		outputDir := f.OutputDir
		if outputDir == "" {
			outputDir = "dist"
		}
		f.Path = filepath.Join(outputDir, "chart", "templates", "NOTES.txt")
	}

	f.TemplateBody = f.generateNotesTemplate()

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

// generateNotesTemplate creates the NOTES.txt content with project-specific information
func (f *Notes) generateNotesTemplate() string {
	return notesTemplate
}

const notesTemplate = `Thank you for installing {{` + "`" + `{{ .Chart.Name }}` + "`" + `}}.

Your release is named {{` + "`" + `{{ .Release.Name }}` + "`" + `}}.

The controller and CRDs have been installed in namespace {{` + "`" + `{{ .Release.Namespace }}` + "`" + `}}.

To verify the installation:

  kubectl get pods -n {{` + "`" + `{{ .Release.Namespace }}` + "`" + `}}
  kubectl get customresourcedefinitions

To learn more about the release, try:

  $ helm status {{` + "`" + `{{ .Release.Name }}` + "`" + `}} -n {{` + "`" + `{{ .Release.Namespace }}` + "`" + `}}
  $ helm get all {{` + "`" + `{{ .Release.Name }}` + "`" + `}} -n {{` + "`" + `{{ .Release.Namespace }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart-templates/notes_test.go
================================================
/*
Copyright 2025 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 charttemplates

import (
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("Notes", func() {
	Context("SetTemplateDefaults", func() {
		var notes *Notes

		BeforeEach(func() {
			notes = &Notes{
				OutputDir: "dist",
				Force:     true,
			}
			notes.InjectProjectName("test-project")
		})

		It("should set the correct path", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.Path).To(Equal("dist/chart/templates/NOTES.txt"))
		})

		It("should use default output dir when not specified", func() {
			notes.OutputDir = ""
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.Path).To(Equal("dist/chart/templates/NOTES.txt"))
		})

		It("should set OverwriteFile action when Force is true", func() {
			notes.Force = true
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.IfExistsAction).To(Equal(machinery.OverwriteFile))
		})

		It("should set SkipFile action when Force is false", func() {
			notes.Force = false
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.IfExistsAction).To(Equal(machinery.SkipFile))
		})

		It("should generate template with Helm template syntax", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.TemplateBody).To(ContainSubstring("{{ .Chart.Name }}"))
			Expect(notes.TemplateBody).To(ContainSubstring("{{ .Release.Name }}"))
			Expect(notes.TemplateBody).To(ContainSubstring("{{ .Release.Namespace }}"))
		})

		It("should include basic installation info", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.TemplateBody).To(ContainSubstring("Thank you for installing"))
			Expect(notes.TemplateBody).To(ContainSubstring("release is named"))
			Expect(notes.TemplateBody).To(ContainSubstring("controller and CRDs have been installed"))
		})

		It("should include kubectl commands for verification", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.TemplateBody).To(ContainSubstring("kubectl get pods"))
			Expect(notes.TemplateBody).To(ContainSubstring("kubectl get customresourcedefinitions"))
		})

		It("should include helm status commands", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			Expect(notes.TemplateBody).To(ContainSubstring("helm status"))
			Expect(notes.TemplateBody).To(ContainSubstring("helm get all"))
		})

		It("should not contain line numbers or metadata", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			// Template should be clean without any Go code artifacts
			Expect(notes.TemplateBody).NotTo(ContainSubstring("LINE_NUMBER"))
			Expect(notes.TemplateBody).NotTo(MatchRegexp(`(?m)^\s*\d+\|`))
		})

		It("should use proper Helm template delimiters", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			// Check for balanced template delimiters
			openCount := strings.Count(notes.TemplateBody, "{{")
			closeCount := strings.Count(notes.TemplateBody, "}}")
			Expect(openCount).To(Equal(closeCount), "Template should have balanced {{ and }} delimiters")
		})

		It("should be concise and generic", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
			// Should be simple and not overly verbose (reasonable limit for helpful content)
			Expect(len(notes.TemplateBody)).To(BeNumerically("<", 800), "NOTES.txt should be concise")
		})

		It("should generate valid Helm template syntax when processed", func() {
			err := notes.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())

			// The template body should use backtick-wrapped syntax for Helm templates
			Expect(notes.TemplateBody).To(ContainSubstring("{{`{{ .Chart.Name }}`}}"))
			Expect(notes.TemplateBody).To(ContainSubstring("{{`{{ .Release.Name }}`}}"))
			Expect(notes.TemplateBody).To(ContainSubstring("{{`{{ .Release.Namespace }}`}}"))
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart-templates/servicemonitor.go
================================================
/*
Copyright 2025 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 charttemplates

import (
	"fmt"
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &ServiceMonitor{}

// ServiceMonitor scaffolds a ServiceMonitor for Prometheus monitoring in the Helm chart
type ServiceMonitor struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// ServiceName is the full name of the metrics service, derived from Kustomize
	ServiceName string

	// OutputDir specifies the output directory for the chart
	OutputDir string
	// Force if true allows overwriting the scaffolded file
	Force bool
}

// SetTemplateDefaults implements machinery.Template
func (f *ServiceMonitor) SetTemplateDefaults() error {
	if f.Path == "" {
		outputDir := f.OutputDir
		if outputDir == "" {
			outputDir = defaultOutputDir
		}
		f.Path = filepath.Join(outputDir, "chart", "templates", "monitoring", "servicemonitor.yaml")
	}

	chartName := f.ProjectName
	f.TemplateBody = fmt.Sprintf(serviceMonitorTemplate, chartName, chartName, chartName, chartName)

	f.IfExistsAction = machinery.OverwriteFile

	return nil
}

const serviceMonitorTemplate = `{{` + "`" + `{{- if .Values.prometheus.enable }}` + "`" + `}}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ "{{ .Release.Service }}" }}
    app.kubernetes.io/name: {{ "{{ include \"%s.name\" . }}" }}
    helm.sh/chart: {{ "{{ .Chart.Name }}-{{ .Chart.Version | replace \"+\" \"_\" }}" }}
    app.kubernetes.io/instance: {{ "{{ .Release.Name }}" }}
    control-plane: controller-manager
  name: ` +
	`{{ "{{ include \"%s.resourceName\" " }}` +
	`{{ "(dict \"suffix\" \"controller-manager-metrics-monitor\" \"context\" $) }}" }}
  namespace: {{ "{{ .Release.Namespace }}" }}
spec:
  endpoints:
    - path: /metrics
      port: https
      scheme: https
      bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
      tlsConfig:
        {{ "{{- if .Values.certManager.enable }}" }}
        serverName: ` +
	`{{ "{{ include \"%s.resourceName\" " }}` +
	`{{ "(dict \"suffix\" \"controller-manager-metrics-service\" \"context\" $) }}" }}.` +
	`{{ "{{ .Release.Namespace }}" }}.svc
        # Apply secure TLS configuration with cert-manager
        insecureSkipVerify: false
        ca:
          secret:
            name: metrics-server-cert
            key: ca.crt
        cert:
          secret:
            name: metrics-server-cert
            key: tls.crt
        keySecret:
          name: metrics-server-cert
          key: tls.key
        {{ "{{- else }}" }}
        # Development/Test mode (insecure configuration)
        insecureSkipVerify: true
        {{ "{{- end }}" }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ "{{ include \"%s.name\" . }}" }}
      control-plane: controller-manager
{{` + "`" + `{{- end }}` + "`" + `}}
`


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart-templates/suite_test.go
================================================
/*
Copyright 2025 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 charttemplates

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestChartTemplates(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Chart Templates Suite")
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/chart.go
================================================
/*
Copyright 2025 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 templates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

const defaultOutputDir = "dist"

var _ machinery.Template = &HelmChart{}

// HelmChart scaffolds a file that defines the Helm chart structure
type HelmChart struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// OutputDir specifies the output directory for the chart
	OutputDir string
	// Force if true allows overwriting the scaffolded file
	Force bool
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmChart) SetTemplateDefaults() error {
	if f.Path == "" {
		outputDir := f.OutputDir
		if outputDir == "" {
			outputDir = defaultOutputDir
		}
		f.Path = filepath.Join(outputDir, "chart", "Chart.yaml")
	}

	f.TemplateBody = helmChartTemplate

	// Chart.yaml is never overwritten as it contains user-managed version info
	f.IfExistsAction = machinery.SkipFile

	return nil
}

const helmChartTemplate = `apiVersion: v2
name: {{ .ProjectName }}
description: A Helm chart to distribute {{ .ProjectName }}
type: application

version: 0.1.0
appVersion: "0.1.0"

keywords:
  - kubernetes
  - operator

annotations:
  kubebuilder.io/generated-by: kubebuilder
`


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/github/test_chart.go
================================================
/*
Copyright 2025 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 github

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmChartCI{}

// HelmChartCI scaffolds the GitHub Action for testing Helm charts
type HelmChartCI struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// Force if true allows overwriting the scaffolded file
	Force bool
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmChartCI) SetTemplateDefaults() error {
	if f.Path == "" {
		f.Path = filepath.Join(".github", "workflows", "test-chart.yml")
	}

	f.TemplateBody = testChartTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

const testChartTemplate = `name: Test Chart

on:
  push:
  pull_request:

jobs:
  test-e2e:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install the latest version of kind
        run: |
          curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind

      - name: Verify kind installation
        run: kind version

      - name: Create kind cluster
        run: kind create cluster

      - name: Prepare {{ .ProjectName }}
        run: |
          go mod tidy
          make docker-build IMG=controller:latest
          kind load docker-image controller:latest

      - name: Install Helm
        run: make install-helm

      - name: Lint Helm Chart
        run: |
          helm lint ./dist/chart

# TODO: Uncomment if cert-manager is enabled
#      - name: Install cert-manager via Helm (wait for readiness)
#        run: |
#          helm repo add jetstack https://charts.jetstack.io
#          helm repo update
#          helm install cert-manager jetstack/cert-manager \
#            --namespace cert-manager \
#            --create-namespace \
#            --set crds.enabled=true \
#            --wait \
#            --timeout 300s

# TODO: Uncomment if Prometheus is enabled
#      - name: Install Prometheus Operator CRDs
#        run: |
#          helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
#          helm repo update
#          helm install prometheus-crds prometheus-community/prometheus-operator-crds

      - name: Deploy manager via Helm
        run: |
          make helm-deploy IMG={{ .ProjectName }}:v0.1.0

      - name: Check Helm release status
        run: |
          make helm-status
`


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/helmignore.go
================================================
/*
Copyright 2025 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 templates

import (
	"path/filepath"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmIgnore{}

// HelmIgnore scaffolds a file that defines the .helmignore for Helm packaging
type HelmIgnore struct {
	machinery.TemplateMixin

	// OutputDir specifies the output directory for the chart
	OutputDir string
	// Force if true allows overwriting the scaffolded file
	Force bool
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmIgnore) SetTemplateDefaults() error {
	if f.Path == "" {
		outputDir := f.OutputDir
		if outputDir == "" {
			outputDir = "dist"
		}
		f.Path = filepath.Join(outputDir, "chart", ".helmignore")
	}

	f.TemplateBody = helmIgnoreTemplate

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

const helmIgnoreTemplate = `# Patterns to ignore when building Helm packages.
# Operating system files
.DS_Store

# Version control directories
.git/
.gitignore
.bzr/
.hg/
.hgignore
.svn/

# Backup and temporary files
*.swp
*.tmp
*.bak
*.orig
*~

# IDE and editor-related files
.idea/
.vscode/

# Helm chart artifacts
dist/chart/*.tgz
`


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/suite_test.go
================================================
/*
Copyright 2025 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 templates

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestTemplates(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Templates Suite")
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic.go
================================================
/*
Copyright 2025 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 templates

import (
	"bytes"
	"fmt"
	"path/filepath"
	"strings"

	"sigs.k8s.io/yaml"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ machinery.Template = &HelmValuesBasic{}

// HelmValuesBasic scaffolds a basic values.yaml based on detected features
type HelmValuesBasic struct {
	machinery.TemplateMixin
	machinery.ProjectNameMixin

	// DeploymentConfig stores extracted deployment configuration (env, resources, security contexts)
	DeploymentConfig map[string]any
	// OutputDir specifies the output directory for the chart
	OutputDir string
	// Force if true allows overwriting the scaffolded file
	Force bool
	// HasWebhooks is true when webhooks were found in the config
	HasWebhooks bool
	// HasMetrics is true when metrics service/monitor were found in the config
	HasMetrics bool
}

// SetTemplateDefaults implements machinery.Template
func (f *HelmValuesBasic) SetTemplateDefaults() error {
	if f.Path == "" {
		outputDir := f.OutputDir
		if outputDir == "" {
			outputDir = "dist"
		}
		f.Path = filepath.Join(outputDir, "chart", "values.yaml")
	}

	f.TemplateBody = f.generateBasicValues()

	if f.Force {
		f.IfExistsAction = machinery.OverwriteFile
	} else {
		f.IfExistsAction = machinery.SkipFile
	}

	return nil
}

// generateBasicValues creates a basic values.yaml based on detected features
func (f *HelmValuesBasic) generateBasicValues() string {
	var buf bytes.Buffer

	// Controller Manager configuration
	imageRepo := "controller"
	imageTag := "latest"
	imagePullPolicy := "IfNotPresent"
	if f.DeploymentConfig != nil {
		if imgCfg, ok := f.DeploymentConfig["image"].(map[string]any); ok {
			if repo, ok := imgCfg["repository"].(string); ok && repo != "" {
				imageRepo = repo
			}
			if tag, ok := imgCfg["tag"].(string); ok && tag != "" {
				imageTag = tag
			}
			if policy, ok := imgCfg["pullPolicy"].(string); ok && policy != "" {
				imagePullPolicy = policy
			}
		}
	}

	buf.WriteString(fmt.Sprintf(`## String to partially override chart.fullname template (will maintain the release name)
##
# nameOverride: ""

## String to fully override chart.fullname template
##
# fullnameOverride: ""

## Configure the controller manager deployment
##
manager:
  replicas: 1

  image:
    repository: %s
    tag: %s
    pullPolicy: %s

`, imageRepo, imageTag, imagePullPolicy))

	// Add extracted deployment configuration
	f.addDeploymentConfig(&buf)

	// RBAC configuration
	buf.WriteString(`## Helper RBAC roles for managing custom resources
##
rbacHelpers:
  # Install convenience admin/editor/viewer roles for CRDs
  enable: false

`)

	// CRD configuration
	buf.WriteString(`## Custom Resource Definitions
##
crd:
  # Install CRDs with the chart
  enable: true
  # Keep CRDs when uninstalling
  keep: true

`)

	// Metrics configuration (enable if metrics artifacts detected in kustomize output)
	metricsPort := 8443
	if f.DeploymentConfig != nil {
		if mp, ok := f.DeploymentConfig["metricsPort"].(int); ok && mp > 0 {
			metricsPort = mp
		}
	}

	if f.HasMetrics {
		buf.WriteString(fmt.Sprintf(`## Controller metrics endpoint.
## Enable to expose /metrics endpoint with RBAC protection.
##
metrics:
  enable: true
  # Metrics server port
  port: %d

`, metricsPort))
	} else {
		buf.WriteString(fmt.Sprintf(`## Controller metrics endpoint.
## Enable to expose /metrics endpoint with RBAC protection.
##
metrics:
  enable: false
  # Metrics server port
  port: %d

`, metricsPort))
	}

	// Cert-manager configuration (always present, enabled based on webhooks)
	if f.HasWebhooks {
		buf.WriteString(`## Cert-manager integration for TLS certificates.
## Required for webhook certificates and metrics endpoint certificates.
##
certManager:
  enable: true

`)
	} else {
		buf.WriteString(`## Cert-manager integration for TLS certificates.
## Required for webhook certificates and metrics endpoint certificates.
##
certManager:
  enable: false

`)
	}

	// Webhook configuration - only if webhooks are present
	if f.HasWebhooks {
		webhookPort := 9443
		if f.DeploymentConfig != nil {
			if wp, ok := f.DeploymentConfig["webhookPort"].(int); ok && wp > 0 {
				webhookPort = wp
			}
		}

		buf.WriteString(fmt.Sprintf(`## Webhook server configuration
##
webhook:
  enable: true
  # Webhook server port
  port: %d

`, webhookPort))
	}

	// Prometheus configuration
	buf.WriteString(`## Prometheus ServiceMonitor for metrics scraping.
## Requires prometheus-operator to be installed in the cluster.
##
prometheus:
  enable: false
`)

	buf.WriteString("\n")
	return buf.String()
}

// addDeploymentConfig adds extracted deployment configuration to the values
func (f *HelmValuesBasic) addDeploymentConfig(buf *bytes.Buffer) {
	f.addArgsSection(buf)

	if f.DeploymentConfig == nil {
		// Add default sections with examples
		f.addDefaultDeploymentSections(buf)
		return
	}

	f.addEnvSection(buf)

	// Add image pull secrets
	if imagePullSecrets, exists := f.DeploymentConfig["imagePullSecrets"]; exists && imagePullSecrets != nil {
		buf.WriteString("  ## Image pull secrets\n")
		buf.WriteString("  ##\n")
		buf.WriteString("  imagePullSecrets:\n")
		if imagePullSecretsYaml, err := yaml.Marshal(imagePullSecrets); err == nil {
			lines := bytes.SplitSeq(imagePullSecretsYaml, []byte("\n"))
			for line := range lines {
				if len(line) > 0 {
					buf.WriteString("    ")
					buf.Write(line)
					buf.WriteString("\n")
				}
			}
		}
		buf.WriteString("\n")
	} else {
		f.addDefaultImagePullSecrets(buf)
	}

	// Add podSecurityContext
	if podSecCtx, exists := f.DeploymentConfig["podSecurityContext"]; exists && podSecCtx != nil {
		buf.WriteString("  ## Pod-level security settings\n")
		buf.WriteString("  ##\n")
		buf.WriteString("  podSecurityContext:\n")
		if secYaml, err := yaml.Marshal(podSecCtx); err == nil {
			f.IndentYamlProperly(buf, secYaml)
		}
		buf.WriteString("\n")
	} else {
		f.addDefaultPodSecurityContext(buf)
	}

	// Add securityContext
	if secCtx, exists := f.DeploymentConfig["securityContext"]; exists && secCtx != nil {
		buf.WriteString("  ## Container-level security settings\n")
		buf.WriteString("  ##\n")
		buf.WriteString("  securityContext:\n")
		if secYaml, err := yaml.Marshal(secCtx); err == nil {
			f.IndentYamlProperly(buf, secYaml)
		}
		buf.WriteString("\n")
	} else {
		f.addDefaultSecurityContext(buf)
	}

	// Add resources
	if resources, exists := f.DeploymentConfig["resources"]; exists && resources != nil {
		buf.WriteString("  ## Resource limits and requests\n")
		buf.WriteString("  ##\n")
		buf.WriteString("  resources:\n")
		if resYaml, err := yaml.Marshal(resources); err == nil {
			f.IndentYamlProperly(buf, resYaml)
		}
		buf.WriteString("\n")
	} else {
		f.addDefaultResources(buf)
	}

	buf.WriteString("  ## Manager pod's affinity\n")
	buf.WriteString("  ##\n")
	if affinity, exists := f.DeploymentConfig["podAffinity"]; exists && affinity != nil {
		buf.WriteString("  affinity:\n")
		if affYaml, err := yaml.Marshal(affinity); err == nil {
			f.IndentYamlProperly(buf, affYaml)
		}
		buf.WriteString("\n")
	} else {
		buf.WriteString("  affinity: {}\n")
		buf.WriteString("\n")
	}

	buf.WriteString("  ## Manager pod's node selector\n")
	buf.WriteString("  ##\n")
	if nodeSelector, exists := f.DeploymentConfig["podNodeSelector"]; exists && nodeSelector != nil {
		buf.WriteString("  nodeSelector:\n")
		if nodYaml, err := yaml.Marshal(nodeSelector); err == nil {
			f.IndentYamlProperly(buf, nodYaml)
		}
		buf.WriteString("\n")
	} else {
		buf.WriteString("  nodeSelector: {}\n")
		buf.WriteString("\n")
	}

	buf.WriteString("  ## Manager pod's tolerations\n")
	buf.WriteString("  ##\n")
	if tolerations, exists := f.DeploymentConfig["podTolerations"]; exists && tolerations != nil {
		buf.WriteString("  tolerations:\n")
		if tolYaml, err := yaml.Marshal(tolerations); err == nil {
			f.IndentYamlProperly(buf, tolYaml)
		}
		buf.WriteString("\n")
	} else {
		buf.WriteString("  tolerations: []\n")
		buf.WriteString("\n")
	}
}

func (f *HelmValuesBasic) IndentYamlProperly(buf *bytes.Buffer, envYaml []byte) {
	lines := bytes.SplitSeq(envYaml, []byte("\n"))
	for line := range lines {
		if len(line) > 0 {
			buf.WriteString("    ")
			buf.Write(line)
			buf.WriteString("\n")
		}
	}
}

// addEnvSection writes env (list, same as master) and only adds envOverrides for CLI.
func (f *HelmValuesBasic) addEnvSection(buf *bytes.Buffer) {
	buf.WriteString("  ## Environment variables\n")
	buf.WriteString("  ##\n")
	if env, exists := f.DeploymentConfig["env"]; exists && env != nil {
		if list, ok := env.([]any); ok && len(list) > 0 {
			buf.WriteString("  env:\n")
			if envYaml, err := yaml.Marshal(list); err == nil {
				f.IndentYamlProperly(buf, envYaml)
			}
		} else {
			buf.WriteString("  env: []\n")
		}
	} else {
		buf.WriteString("  env: []\n")
	}
	buf.WriteString("\n")
	buf.WriteString("  ## Env overrides (--set manager.envOverrides.VAR=value)\n")
	buf.WriteString("  ## Same name in env above: this value takes precedence.\n")
	buf.WriteString("  ##\n")
	buf.WriteString("  envOverrides: {}\n")
	buf.WriteString("\n")
}

// addDefaultDeploymentSections adds default sections when no deployment config is available
func (f *HelmValuesBasic) addDefaultDeploymentSections(buf *bytes.Buffer) {
	buf.WriteString("  ## Environment variables\n")
	buf.WriteString("  ##\n")
	buf.WriteString("  env: []\n")
	buf.WriteString("\n")
	buf.WriteString("  ## Env overrides (--set manager.envOverrides.VAR=value)\n")
	buf.WriteString("  ## Same name in env above: this value takes precedence.\n")
	buf.WriteString("  ##\n")
	buf.WriteString("  envOverrides: {}\n")
	buf.WriteString("\n")

	f.addDefaultImagePullSecrets(buf)
	f.addDefaultPodSecurityContext(buf)
	f.addDefaultSecurityContext(buf)
	f.addDefaultResources(buf)
}

// addArgsSection adds controller manager args section to the values file
func (f *HelmValuesBasic) addArgsSection(buf *bytes.Buffer) {
	buf.WriteString("  ## Arguments\n  ##\n")

	if f.DeploymentConfig != nil {
		if args, exists := f.DeploymentConfig["args"]; exists && args != nil {
			if argsYaml, err := yaml.Marshal(args); err == nil {
				if trimmed := strings.TrimSpace(string(argsYaml)); trimmed != "" && trimmed != "[]" {
					lines := bytes.Split(argsYaml, []byte("\n"))
					buf.WriteString("  args:\n")
					for _, line := range lines {
						if len(line) > 0 {
							buf.WriteString("    ")
							buf.Write(line)
							buf.WriteString("\n")
						}
					}
					buf.WriteString("\n")
					return
				}
			}
		}
	}

	buf.WriteString("  args: []\n\n")
}

// addDefaultImagePullSecrets adds default imagePullSecrets section
func (f *HelmValuesBasic) addDefaultImagePullSecrets(buf *bytes.Buffer) {
	buf.WriteString("  ## Image pull secrets\n")
	buf.WriteString("  ##\n")
	buf.WriteString("  imagePullSecrets: []\n\n")
}

// addDefaultPodSecurityContext adds default podSecurityContext section
func (f *HelmValuesBasic) addDefaultPodSecurityContext(buf *bytes.Buffer) {
	buf.WriteString("  ## Pod-level security settings\n")
	buf.WriteString("  ##\n")
	buf.WriteString("  podSecurityContext: {}\n")
	buf.WriteString("    # fsGroup: 2000\n\n")
}

// addDefaultSecurityContext adds default securityContext section
func (f *HelmValuesBasic) addDefaultSecurityContext(buf *bytes.Buffer) {
	buf.WriteString("  ## Container-level security settings\n")
	buf.WriteString("  ##\n")
	buf.WriteString("  securityContext: {}\n")
	buf.WriteString("    # capabilities:\n")
	buf.WriteString("    #   drop:\n")
	buf.WriteString("    #   - ALL\n")
	buf.WriteString("    # readOnlyRootFilesystem: true\n")
	buf.WriteString("    # runAsNonRoot: true\n")
	buf.WriteString("    # runAsUser: 1000\n\n")
}

// addDefaultResources adds default resources section
func (f *HelmValuesBasic) addDefaultResources(buf *bytes.Buffer) {
	buf.WriteString("  ## Resource limits and requests\n")
	buf.WriteString("  ##\n")
	buf.WriteString("  resources: {}\n")
	buf.WriteString("    # limits:\n")
	buf.WriteString("    #   cpu: 100m\n")
	buf.WriteString("    #   memory: 128Mi\n")
	buf.WriteString("    # requests:\n")
	buf.WriteString("    #   cpu: 100m\n")
	buf.WriteString("    #   memory: 128Mi\n\n")
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/internal/templates/values_basic_test.go
================================================
/*
Copyright 2025 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 templates

import (
	"strings"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

var _ = Describe("HelmValuesBasic", func() {
	var valuesTemplate *HelmValuesBasic

	Context("when project has webhooks", func() {
		BeforeEach(func() {
			valuesTemplate = &HelmValuesBasic{
				HasWebhooks:      true,
				DeploymentConfig: map[string]any{},
			}
			valuesTemplate.InjectProjectName("test-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should include certManager configuration", func() {
			content := valuesTemplate.GetBody()

			Expect(content).To(ContainSubstring("certManager:"))
			Expect(content).To(ContainSubstring("enable: true"))
		})

		It("should include all basic sections", func() {
			content := valuesTemplate.GetBody()

			Expect(content).To(ContainSubstring("manager:"))
			Expect(content).To(ContainSubstring("args: []"))
			Expect(content).To(ContainSubstring("env: []"))
			Expect(content).To(ContainSubstring("envOverrides: {}"))
			Expect(content).To(ContainSubstring("metrics:"))
			Expect(content).To(ContainSubstring("prometheus:"))
			Expect(content).To(ContainSubstring("rbacHelpers:"))
			Expect(content).To(ContainSubstring("imagePullSecrets: []"))
		})

		It("should include env list and envOverrides for CLI", func() {
			content := valuesTemplate.GetBody()
			Expect(content).To(ContainSubstring("env: []"))
			Expect(content).To(ContainSubstring("envOverrides: {}"))
			Expect(content).To(ContainSubstring("--set manager.envOverrides.VAR=value"))
		})
	})

	Context("when project has no webhooks", func() {
		BeforeEach(func() {
			valuesTemplate = &HelmValuesBasic{
				HasWebhooks:      false,
				DeploymentConfig: map[string]any{},
			}
			valuesTemplate.InjectProjectName("test-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should not include certManager configuration", func() {
			content := valuesTemplate.GetBody()

			Expect(content).To(ContainSubstring("certManager:"))
			Expect(content).To(ContainSubstring("enable: false"))
		})

		It("should still include other basic sections", func() {
			content := valuesTemplate.GetBody()

			Expect(content).To(ContainSubstring("manager:"))
			Expect(content).To(ContainSubstring("args: []"))
			Expect(content).To(ContainSubstring("metrics:"))
			Expect(content).To(ContainSubstring("prometheus:"))
			Expect(content).To(ContainSubstring("rbacHelpers:"))
			Expect(content).To(ContainSubstring("imagePullSecrets: []"))
		})
	})

	Context("template path and content", func() {
		BeforeEach(func() {
			valuesTemplate = &HelmValuesBasic{
				OutputDir: "dist",
			}
			valuesTemplate.InjectProjectName("test-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should have correct path", func() {
			Expect(valuesTemplate.GetPath()).To(Equal("dist/chart/values.yaml"))
		})

		It("should implement Builder interface", func() {
			var builder machinery.Builder = valuesTemplate
			Expect(builder).NotTo(BeNil())
		})

		It("should have correct file permissions", func() {
			info := valuesTemplate.GetIfExistsAction()
			Expect(info).To(Equal(machinery.SkipFile))
		})
	})

	Context("with deployment configuration", func() {
		BeforeEach(func() {
			deploymentConfig := map[string]any{
				"args": []any{
					"--leader-elect",
				},
				"env": []any{
					map[string]any{
						"name":  "TEST_ENV",
						"value": "test-value",
					},
				},
				"image": map[string]any{
					"repository": "example.com/custom-controller",
					"tag":        "v1.2.3",
					"pullPolicy": "Always",
				},
				"resources": map[string]any{
					"limits": map[string]any{
						"cpu":    "100m",
						"memory": "128Mi",
					},
				},
			}

			valuesTemplate = &HelmValuesBasic{
				HasWebhooks:      false,
				DeploymentConfig: deploymentConfig,
			}
			valuesTemplate.InjectProjectName("test-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should include deployment configuration", func() {
			content := valuesTemplate.GetBody()
			Expect(content).To(ContainSubstring("args:"))
			Expect(content).To(ContainSubstring("- --leader-elect"))
			Expect(content).To(ContainSubstring("env:"))
			Expect(content).To(ContainSubstring("TEST_ENV"))
			Expect(content).To(ContainSubstring("test-value"))
			Expect(content).To(ContainSubstring("repository: example.com/custom-controller"))
			Expect(content).To(ContainSubstring("tag: v1.2.3"))
			Expect(content).To(ContainSubstring("pullPolicy: Always"))
			Expect(content).To(ContainSubstring("resources:"))
			Expect(content).To(ContainSubstring("cpu: 100m"))
			Expect(content).To(ContainSubstring("memory: 128Mi"))
			Expect(content).To(ContainSubstring("affinity: {}"))
			Expect(content).To(ContainSubstring("nodeSelector: {}"))
			Expect(content).To(ContainSubstring("tolerations: []"))
		})
	})

	Context("with nodeSelector, affinity and tolerations configuration", func() {
		BeforeEach(func() {
			deploymentConfig := map[string]any{
				"podNodeSelector": map[string]string{
					"kubernetes.io/os": "linux",
				},
				"podTolerations": []map[string]string{
					{
						"key":      "key1",
						"operator": "Equal",
						"effect":   "NoSchedule",
					},
				},
				"podAffinity": map[string]any{
					"nodeAffinity": map[string]any{
						"requiredDuringSchedulingIgnoredDuringExecution": map[string]any{
							"nodeSelectorTerms": []any{
								map[string]any{
									"matchExpressions": []any{
										map[string]any{
											"key":      "topology.kubernetes.io/zone",
											"operator": "In",
											"values":   []string{"antarctica-east1", "antarctica-east2"},
										},
									},
								},
							},
						},
					},
				},
			}

			valuesTemplate = &HelmValuesBasic{
				HasWebhooks:      false,
				DeploymentConfig: deploymentConfig,
			}
			valuesTemplate.InjectProjectName("test-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should include default values", func() {
			content := valuesTemplate.GetBody()
			Expect(content).To(ContainSubstring(`  ## Manager pod's node selector
  ##
  nodeSelector:
    kubernetes.io/os: linux`))

			Expect(content).To(ContainSubstring(`  ## Manager pod's tolerations
  ##
  tolerations:
    - effect: NoSchedule
      key: key1
      operator: Equal`))

			Expect(content).To(ContainSubstring(`  ## Manager pod's affinity
  ##
  affinity:
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: topology.kubernetes.io/zone
            operator: In
            values:
            - antarctica-east1
            - antarctica-east2`))
		})
	})

	Context("with multiple imagePullSecrets", func() {
		BeforeEach(func() {
			valuesTemplate = &HelmValuesBasic{
				DeploymentConfig: map[string]any{
					"imagePullSecrets": []any{
						map[string]any{
							"name": "test-secret",
						},
						map[string]any{
							"name": "test-secret2",
						},
					},
				},
			}
			valuesTemplate.InjectProjectName("test-private-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should render multiple imagePullSecrets", func() {
			content := valuesTemplate.GetBody()
			Expect(content).To(ContainSubstring("imagePullSecrets:"))
			Expect(content).To(ContainSubstring("- name: test-secret"))
			Expect(content).To(ContainSubstring("- name: test-secret2"))
		})
	})

	Context("with complex env variables", func() {
		BeforeEach(func() {
			valuesTemplate = &HelmValuesBasic{
				DeploymentConfig: map[string]any{
					"env": []any{
						map[string]any{
							"name": "POD_NAMESPACE",
							"valueFrom": map[string]any{
								"fieldRef": map[string]any{
									"fieldPath": "metadata.namespace",
								},
							},
						},
					},
				},
			}
			valuesTemplate.InjectProjectName("test-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should render nested env configuration", func() {
			content := valuesTemplate.GetBody()
			Expect(content).To(ContainSubstring("env:"))
			Expect(content).To(ContainSubstring("POD_NAMESPACE"))
			Expect(content).To(ContainSubstring("valueFrom:"))
			Expect(content).To(ContainSubstring("fieldRef:"))
			Expect(content).To(ContainSubstring("fieldPath: metadata.namespace"))
		})
	})

	Context("rbacHelpers configuration", func() {
		BeforeEach(func() {
			valuesTemplate = &HelmValuesBasic{
				HasWebhooks: false,
			}
			valuesTemplate.InjectProjectName("test-project")
			err := valuesTemplate.SetTemplateDefaults()
			Expect(err).NotTo(HaveOccurred())
		})

		It("should have rbacHelpers disabled by default", func() {
			content := valuesTemplate.GetBody()
			Expect(content).To(ContainSubstring("rbacHelpers:"))
			Expect(content).To(ContainSubstring("enable: false"))
		})
	})

	Context("Port configuration", func() {
		Context("with default ports", func() {
			BeforeEach(func() {
				valuesTemplate = &HelmValuesBasic{
					HasWebhooks:      true,
					HasMetrics:       true,
					DeploymentConfig: map[string]any{},
				}
				valuesTemplate.InjectProjectName("test-project")
				err := valuesTemplate.SetTemplateDefaults()
				Expect(err).NotTo(HaveOccurred())
			})

			It("should include default webhook port", func() {
				content := valuesTemplate.GetBody()
				Expect(content).To(ContainSubstring("webhook:"))
				Expect(content).To(ContainSubstring("enable: true"))
				Expect(content).To(ContainSubstring("port: 9443"))
			})

			It("should include certManager enabled", func() {
				content := valuesTemplate.GetBody()
				Expect(content).To(ContainSubstring("certManager:"))
				Expect(content).To(ContainSubstring("enable: true"))
			})

			It("should include default metrics port", func() {
				content := valuesTemplate.GetBody()
				Expect(content).To(ContainSubstring("metrics:"))
				Expect(content).To(ContainSubstring("enable: true"))
				Expect(content).To(ContainSubstring("port: 8443"))
			})

			It("should not expose health port in values", func() {
				content := valuesTemplate.GetBody()
				Expect(content).NotTo(ContainSubstring("healthPort"))
			})
		})

		Context("with custom ports extracted from deployment", func() {
			BeforeEach(func() {
				deploymentConfig := map[string]any{
					"webhookPort": 9444,
					"metricsPort": 9090,
				}

				valuesTemplate = &HelmValuesBasic{
					HasWebhooks:      true,
					HasMetrics:       true,
					DeploymentConfig: deploymentConfig,
				}
				valuesTemplate.InjectProjectName("test-project")
				err := valuesTemplate.SetTemplateDefaults()
				Expect(err).NotTo(HaveOccurred())
			})

			It("should use custom webhook port", func() {
				content := valuesTemplate.GetBody()
				Expect(content).To(ContainSubstring("webhook:"))
				Expect(content).To(ContainSubstring("port: 9444"))
				Expect(content).NotTo(ContainSubstring("port: 9443"))
			})

			It("should use custom metrics port", func() {
				content := valuesTemplate.GetBody()
				Expect(content).To(ContainSubstring("metrics:"))
				Expect(content).To(ContainSubstring("port: 9090"))
				Expect(content).NotTo(ContainSubstring("port: 8443"))
			})

			It("should not expose health port", func() {
				content := valuesTemplate.GetBody()
				Expect(content).NotTo(ContainSubstring("healthPort"))
			})
		})

		Context("with partial custom ports", func() {
			BeforeEach(func() {
				deploymentConfig := map[string]any{
					"metricsPort": 9090,
					// webhookPort not provided - should use default
				}

				valuesTemplate = &HelmValuesBasic{
					HasWebhooks:      true,
					HasMetrics:       true,
					DeploymentConfig: deploymentConfig,
				}
				valuesTemplate.InjectProjectName("test-project")
				err := valuesTemplate.SetTemplateDefaults()
				Expect(err).NotTo(HaveOccurred())
			})

			It("should use custom metrics port and default webhook port", func() {
				content := valuesTemplate.GetBody()
				Expect(content).To(ContainSubstring("port: 9090")) // custom metrics
				Expect(content).To(ContainSubstring("port: 9443")) // default webhook
			})

			It("should not expose health port", func() {
				content := valuesTemplate.GetBody()
				Expect(content).NotTo(ContainSubstring("healthPort"))
			})
		})

		Context("with no webhooks but with metrics", func() {
			BeforeEach(func() {
				valuesTemplate = &HelmValuesBasic{
					HasWebhooks:      false,
					HasMetrics:       true,
					DeploymentConfig: map[string]any{},
				}
				valuesTemplate.InjectProjectName("test-project")
				err := valuesTemplate.SetTemplateDefaults()
				Expect(err).NotTo(HaveOccurred())
			})

			It("should not include webhook configuration", func() {
				content := valuesTemplate.GetBody()
				Expect(content).NotTo(ContainSubstring("webhook:"))
			})

			It("should include metrics port configuration", func() {
				content := valuesTemplate.GetBody()
				Expect(content).To(ContainSubstring("metrics:"))
				Expect(content).To(ContainSubstring("port: 8443"))
				Expect(content).NotTo(ContainSubstring("healthPort"))
			})
		})

		Context("port configuration structure", func() {
			BeforeEach(func() {
				valuesTemplate = &HelmValuesBasic{
					HasWebhooks:      true,
					HasMetrics:       true,
					DeploymentConfig: map[string]any{},
				}
				valuesTemplate.InjectProjectName("test-project")
				err := valuesTemplate.SetTemplateDefaults()
				Expect(err).NotTo(HaveOccurred())
			})

			It("should have ports under webhook section", func() {
				content := valuesTemplate.GetBody()
				lines := strings.Split(content, "\n")

				var webhookLine, portLine int
				for i, line := range lines {
					if strings.Contains(line, "webhook:") && !strings.Contains(line, "#") {
						webhookLine = i
					}
					if webhookLine > 0 && i > webhookLine && strings.Contains(line, "port:") &&
						strings.Contains(line, "9443") {
						portLine = i
						break
					}
				}

				Expect(webhookLine).To(BeNumerically(">", 0))
				Expect(portLine).To(BeNumerically(">", webhookLine))
				Expect(portLine - webhookLine).To(BeNumerically("<=", 3))
			})

			It("should have port under metrics section", func() {
				content := valuesTemplate.GetBody()
				lines := strings.Split(content, "\n")

				var metricsLine, portLine int
				for i, line := range lines {
					if strings.Contains(line, "metrics:") && !strings.Contains(line, "#") {
						metricsLine = i
					}
					if metricsLine > 0 && i > metricsLine {
						if strings.Contains(line, "port:") && strings.Contains(line, "8443") {
							portLine = i
						}
					}
				}

				Expect(metricsLine).To(BeNumerically(">", 0))
				Expect(portLine).To(BeNumerically(">", metricsLine))
			})
		})
	})
})


================================================
FILE: pkg/plugins/optional/helm/v2alpha/scaffolds/suite_test.go
================================================
//go:build integration

/*
Copyright 2026 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 scaffolds

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestScaffolds(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Scaffolds Suite")
}


================================================
FILE: pkg/plugins/optional/helm/v2alpha/suite_test.go
================================================
/*
Copyright 2025 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 v2alpha

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestHelmV2AlphaPlugin(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Helm v2-alpha Plugin Suite")
}


================================================
FILE: pkg/plugins/scaffolder.go
================================================
/*
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package plugins

import (
	"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)

// Scaffolder interface creates files to set up a controller manager
type Scaffolder interface {
	InjectFS(machinery.Filesystem)
	// Scaffold performs the scaffolding
	Scaffold() error
}


================================================
FILE: roadmap/README.md
================================================
# Kubebuilder Roadmaps

**Welcome to the Kubebuilder Roadmaps directory!**

This space is dedicated to housing the strategic roadmaps for the
Kubebuilder project, organized by year. Each document within this repository
outlines the key initiatives, objectives, and goals for Kubebuilder, reflecting our
commitment to enhancing the development experience within the Kubernetes ecosystem.

Below, you will find links to the roadmap document for each year. These documents provide insights into the
specific objectives set for the project during that time, the motivation behind each goal, and the progress
made towards achieving them:

- [Roadmap 2024](roadmap_2024.md)
- [Roadmap 2025](roadmap_2025.md)
- [Roadmap 2026](roadmap_2026.md)

## :point_right: New plugins/RFEs to provide integrations within other Projects

As Kubebuilder evolves, we prioritize a focused project scope and minimal reliance on third-party dependencies,
concentrating on features that bring the most value to our community.

While recognizing the need for flexibility, we opt not to directly support third-party project integrations.
Instead, we've enhanced Kubebuilder as a library, enabling any project to create compatible plugins.
This approach delegates maintenance to those with the deepest understanding of their projects, fostering higher
quality and community contributions.

We're here to support you in developing your own Kubebuilder plugins.
For guidance on [Creating Your own plugins](https://kubebuilder.io/plugins/creating-plugins).

This strategy empowers our users and contributors to innovate,
keeping Kubebuilder streamlined and focused on essential Kubernetes development functionalities.

**Therefore, our primary objective remains to offer a CLI tool that assists users in developing
solutions for deployment and distribution on Kubernetes clusters using Golang.
We aim to simplify the complexities involved and speed up the development process,
thereby lowering the learning curve.**

## :steam_locomotive: Contributing

Your input and contributions are what make Kubebuilder a continually
evolving and improving project. We encourage the community to participate in discussions,
provide feedback on the roadmaps, and contribute to the development efforts.

If you have suggestions for future objectives or want to get involved
in current initiatives, please refer to our [contributing guidelines](./../CONTRIBUTING.md)
or reach out to the project maintainers. Please, feel free either
to raise new issues and/or Pull Requests against this repository with your
suggestions.

## :loudspeaker: Stay Tuned

For the latest updates, discussions, and contributions to the Kubebuilder project,
please join our community channels and forums. Your involvement is crucial for the
sustained growth and success of Kubebuilder.

**:tada: Thank you for being a part of the Kubebuilder journey.**

Together, we are building the future of Kubernetes development.

## Template for roadmap items

```markdown
### [Goal Title]

**Status:** [Status Emoji] [Short Status Update]

**Objective:** [Brief description of the objective]

**Context:** [Optional - Any relevant background or broader context]

**Motivations:** [Optional - If applicable]
- [Key motivation 1]
- [Key motivation 2]

**Proposed Solutions:** [Optional - If applicable]
- [Solution 1]
- [Solution 2]
- [More as needed]

**References:** [Optional - Links to discussions, PRs, issues, etc.]
- [Reference 1 with URL]
- [Reference 2 with URL]
- [More as needed]
```


================================================
FILE: roadmap/roadmap_2024.md
================================================
# Kubebuilder Project Roadmap 2024

### Updating Scaffolding to Align with the Latest changes of controller-runtime

**Status:** ✅ Complete (Changes available from release `4.3.0`)

**Objective:** Update Kubebuilder's controller scaffolding to align with the latest changes
in controller-runtime, focusing on compatibility and addressing recent updates and deprecations
mainly related to webhooks.

**Context:** Kubebuilder's plugin system is designed for stability, yet it depends on controller-runtime,
which is evolving rapidly with versions still under 1.0.0. Notable changes and deprecations,
especially around webhooks, necessitate Kubebuilder's alignment with the latest practices
and functionalities of controller-runtime. We need update the Kubebuilder scaffolding,
samples, and documentation.

**References:**
- [Issue - Deprecations in Controller-Runtime and Impact on Webhooks](https://github.com/kubernetes-sigs/kubebuilder/issues/3721) - An issue detailing the deprecations in controller-runtime that affect Kubebuilder's approach to webhooks.
- [PR - Update to Align with Latest Controller-Runtime Webhook Interface](https://github.com/kubernetes-sigs/kubebuilder/pull/3399) - A pull request aimed at updating Kubebuilder to match controller-runtime's latest webhook interface.
- [PR - Enhancements to Controller Scaffolding for Upcoming Controller-Runtime Changes](https://github.com/kubernetes-sigs/kubebuilder/pull/3723) - A pull request proposing enhancements to Kubebuilder's controller scaffolding in anticipation of upcoming changes in controller-runtime.


#### (New Optional Plugin) Helm Chart Packaging

**Status:** ✅ Complete ( Initial version merged https://github.com/kubernetes-sigs/kubebuilder/pull/4227 - further improvements and contributions are welcome)

**Objective:** We aim to introduce a new plugin for Kubebuilder that packages projects as Helm charts,
facilitating easier distribution and integration of solutions within the Kubernetes ecosystem. For details on this proposal and how to contribute,
see [GitHub Pull Request #3632](https://github.com/kubernetes-sigs/kubebuilder/pull/3632).

**Motivation:** The growth of the Kubernetes ecosystem underscores the need for flexible and
accessible distribution methods. A Helm chart packaging plugin would simplify the distribution of the solutions
and allow easy integrations with common applications used by administrators.

---
### Transition from Google Cloud Platform (GCP) to build and promote binaries and images

**Status:**
- **Kubebuilder CLI**: :white_check_mark: Complete. It has been built using Go releaser. [More info](./../build/.goreleaser.yml)
- **kube-rbac-proxy Images:**  :white_check_mark: Complete. ([More info](https://github.com/kubernetes-sigs/kubebuilder/discussions/3907))
- **EnvTest binaries:** :white_check_mark: Complete Controller-Runtime maintainers are working in a solution to build them out and take the ownership over this one. More info:
  - https://kubernetes.slack.com/archives/C02MRBMN00Z/p1712457941924299
  - https://kubernetes.slack.com/archives/CCK68P2Q2/p1713174342482079
  - Also, see the PR: https://github.com/kubernetes-sigs/controller-runtime/pull/2811
  - It will be available from the next release v0.19.
- **PR Check image:**  🙌 Seeking Contributions to do the required changes - See that the images used to check the PR titles are also build and promoted by the Kubebuilder project in GCP but are from the project: https://github.com/kubernetes-sigs/kubebuilder-release-tools. The plan in this case is to use the e2e shared infrastructure. [More info](https://github.com/kubernetes/k8s.io/issues/2647#issuecomment-2111182864)

**Objective:** Shift Kubernetes (k8s) project infrastructure from GCP to shared infrastructures.
Furthermore, move from the registry `k8s.gcr.io` to `registry.k8s.io`.

**Motivation:** The initiative to move away from GCP aligns with the broader k8s project's
goal of utilizing shared infrastructures. This transition is crucial for ensuring the availability
of the artifacts in the long run and aligning compliance with other projects under the kubernetes-sig org.
[Issue #2647](https://github.com/kubernetes/k8s.io/issues/2647) provides more details on the move.

**Context:** Currently, Google Cloud is used only for:

- **Rebuild and provide the images for kube-rbac-proxy:**

A particular challenge has been the necessity to rebuild images for the
[kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy), which is in the process of being
donated to kubernetes-sig. This transition was expected to eliminate the need for
continuous re-tagging and rebuilding of its images to ensure their availability to users.
The configuration for building these images is outlined
[here](https://github.com/kubernetes-sigs/kubebuilder/blob/master/RELEASE.md#to-build-the-kube-rbac-proxy-images).

- **Build and Promote EnvTest binaries**:

The development of Kubebuilder Tools and EnvTest binaries,
essential for controller tests, represents another area reliant on k8s binaries
traditionally built within GCP environments. Our documentation on building these artifacts is
available [here](https://github.com/kubernetes-sigs/kubebuilder/blob/master/RELEASE.md#to-build-the-kubebuilder-tools-artifacts-required-to-use-env-test).

**We encourage the Kubebuilder community to participate in this discussion, offering feedback and contributing ideas
to refine these proposals. Your involvement is crucial in shaping the future of secure and efficient project scaffolding in Kubebuilder.**

---
### kube-rbac-proxy's Role in Default Scaffold

**Status:** :white_check_mark: Complete

- **Resolution**: The usage of kube-rbac-proxy has been discontinued from the default scaffold. We plan to provide other helpers to protect the metrics endpoint. Furthermore, once the project is accepted under kubernetes-sig or kubernetes-auth, we may contribute to its maintainer in developing an external plugin for use with projects built with Kubebuilder.
   - **Proposal**: [https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/discontinue_usage_of_kube_rbac_proxy.md](https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/discontinue_usage_of_kube_rbac_proxy.md)
   - **PR**: [https://github.com/kubernetes-sigs/kubebuilder/pull/3899](https://github.com/kubernetes-sigs/kubebuilder/pull/3899)
   - **Communication**: [https://github.com/kubernetes-sigs/kubebuilder/discussions/3907](https://github.com/kubernetes-sigs/kubebuilder/discussions/3907)

**Objective:** Evaluate potential modifications or the exclusion of [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy)
from the default Kubebuilder scaffold in response to deprecations and evolving user requirements.

**Context:** [kube-rbac-proxy](https://github.com/brancz/kube-rbac-proxy) , a key component for securing Kubebuilder-generated projects,
faces significant deprecations that impact automatic certificate generation.
For more insights into these challenges, see [Issue #3524](https://github.com/kubernetes-sigs/kubebuilder/issues/3524).

This situation necessitates a reevaluation of its inclusion and potentially prompts users to
adopt alternatives like cert-manager by default. Additionally, the requirement to manually rebuild
[kube-rbac-proxy images—due](https://github.com/kubernetes-sigs/kubebuilder/blob/master/RELEASE.md#to-build-the-kube-rbac-proxy-images)
to its external status from Kubernetes-SIG—places a considerable maintenance
burden on Kubebuilder maintainers.

**Motivations:**
- Address kube-rbac-proxy breaking changes/deprecations.
  - For further information: [Issue #3524 - kube-rbac-proxy warn about deprecation and future breaking changes](https://github.com/kubernetes-sigs/kubebuilder/issues/3524)
- Feedback from the community has highlighted a preference for cert-manager's default integration, aiming security with Prometheus and metrics.
  - More info: [GitHub Issue #3524 - Improve scaffolding of ServiceMonitor](https://github.com/kubernetes-sigs/kubebuilder/issues/3657)
- Desire for kube-rbac-proxy to be optional, citing its prescriptive nature.
  - See: [Issue #3482 - The kube-rbac-proxy is too opinionated to be opt-out.](https://github.com/kubernetes-sigs/kubebuilder/issues/3482)
- Reduce the maintainability effort to generate the images used by Kubebuilder projects and dependency within third-party solutions.
  - Related issues:
    - [Issue #1885 - use a NetworkPolicy instead of kube-rbac-proxy](https://github.com/kubernetes-sigs/kubebuilder/issues/1885)
    - [Issue #3230 - Migrate away from google.com gcp project kubebuilder](https://github.com/kubernetes-sigs/kubebuilder/issues/3230)

---
### Providing Helpers for Project Distribution

#### Distribution via Kustomize

**Status:** :white_check_mark: Complete

- **Resolution**: As of release ([v3.14.0](https://github.com/kubernetes-sigs/kubebuilder/releases/tag/v3.14.0)), Kubebuilder includes enhanced support for project distribution. Users can now scaffold projects with a `build-installer` makefile target. This improvement enables the straightforward deployment of solutions directly to Kubernetes clusters. Users can deploy their projects using commands like:

```shell
kubectl apply -f https://raw.githubusercontent.com//my-project//dist/install.yaml
```
This enhancement streamlines the process of getting Kubebuilder projects running on clusters, providing a seamless deployment experience.

---
### **(Major Release for Kubebuilder CLI 4.x)** Removing Deprecated Plugins for Enhanced Maintainability and User Experience

**Status:** : ✅ Complete - Release was done
  - **Remove Deprecations**:https://github.com/kubernetes-sigs/kubebuilder/issues/3603
  - **Bump Module**: https://github.com/kubernetes-sigs/kubebuilder/pull/3924

**Objective:** To remove all deprecated plugins from Kubebuilder to improve project maintainability and
enhance user experience. This initiative also includes updating the project documentation to provide clear
and concise information, eliminating any confusion for users. **More Info:** [GitHub Discussion #3622](https://github.com/kubernetes-sigs/kubebuilder/discussions/3622)

**Motivation:** By focusing on removing deprecated plugins—specifically, versions or kinds that can no
longer be supported—we aim to streamline the development process and ensure a higher quality user experience.
Clear and updated documentation will further assist in making development workflows more efficient and less prone to errors.



================================================
FILE: roadmap/roadmap_2025.md
================================================
# Kubebuilder Project Roadmap 2025

## Ensure Webhook Implementation Stability and Enhance User Experience

**Status:** Partially Complete

### Objective
Enhance the webhooks implementation and user experience.

### Context
The current implementations for webhook conversion and defaulting are stable and tested through basic end-to-end (E2E) workflows.
However, webhook conversion is incomplete, and several bugs need to be addressed. Additionally, the user experience
is hindered by limitations such as the inability to add additional webhooks for same API without using the force flag
and losing their existing customizations on top.

### Goals and Needs
- **CA Injection**: ✅ Complete (Changes available from release `4.4.0`) Ensure that CA injection for conversion webhooks is limited to the relevant Custom Resource (CR) conversions.
  - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/4285)
  - [Pull Request](https://github.com/kubernetes-sigs/kubebuilder/pull/4282)

- **Scaffolding Multiple Webhooks**: Allow adding additional webhooks without requiring forced re-scaffolding.
  - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/4146)

- **Hub and Spoke Model**: ✅ Complete (Changes available from release `4.4.0`) Integrate a hub-and-spoke model for conversion webhooks to streamline implementation.
  - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/2589)
  - [Pull Request](https://github.com/kubernetes-sigs/kubebuilder/pull/4254)

- **Comprehensive E2E Testing**: ✅ Complete ([Example](https://github.com/kubernetes-sigs/kubebuilder/blob/v4.7.1/testdata/project-v4-with-plugins/test/e2e/e2e_test.go#L284-L296)) Expand end-to-end tests for conversion webhooks to validate not only CA injection but also the conversion process itself.
  - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/4297)

- **E2E Test Scaffolding**: (✅ Complete) Improve the E2E test scaffolds under `test/e2e` to validate conversion behavior beyond CA injection for conversion webhooks.

- **Enhanced Multiversion Tutorial**: (✅ Complete) Add E2E tests for conversion webhooks in the multiversion tutorial to support comprehensive user guidance.
  - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/4255)

---
# Roadmap Document

## Enhance the Helm Chart Plugin

**Status:** ✅ Complete (Released in Kubebuilder v4.10.0, introduced `helm/v2-alpha` plugin which supersedes the previous version and addressed the community feedback.)

### Context
A new plugin to help users scaffold a Helm chart to distribute their solutions is implemented as an experimental
feature on the `master` branch and is currently under development. It's initial version will be released in
the next major version of Kubebuilder.

### Objective
The objective of this effort is to ensure that the Helm chart plugin addresses user needs effectively while
providing a seamless and intuitive experience.

### Goals
- Prevent exposure of webhooks data in the Helm chart values.
- Determine whether and how to include sample files and CR configurations in the Helm chart.
- Enable users to specify the path where the Helm chart will be scaffolded.

### References
- [Milestone Helm](https://github.com/kubernetes-sigs/kubebuilder/milestone/39)
- [Code Implementation](https://github.com/kubernetes-sigs/kubebuilder/tree/master/pkg/plugins/optional/helm)
- [Sample Under Testdata](https://github.com/kubernetes-sigs/kubebuilder/tree/master/testdata/project-v4-with-plugins/dist/chart)

---

## Align Tutorials and Samples with Best Practices Proposed by DeployImage Plugin

**Status:** ✅ Complete

### Context
The existing tutorials lack consistency with best practices and the layout proposed by the DeployImage plugin.

### Objective
Align tutorials and sample projects with best practices to improve quality and usability.

### Goals
- **Controller Logic Consistency**: Standardize tutorial controller logic to match the DeployImage plugin’s scaffolded controller, including conditions, finalizers, and status updates.

- **Conditional Status in CronJob Spec**: Incorporate conditional status handling in the CronJob spec to reflect best practices.

- **Test Logic Consistency**: Ensure tutorial test logic mirrors the tests scaffolded by the DeployImage plugin, adapting as needed for specific cases.

---

## Provide Solutions to Keep Users Updated with the Latest Changes

**Status:** (✅ feature complete from 4.8.0 )
- Proposal: https://github.com/kubernetes-sigs/kubebuilder/blob/master/designs/update_action.md
- `kubebuilder alpha update` command implemented. More info: https://book.kubebuilder.io/reference/commands/alpha_update
- AutoUpdate Plugin implemented as v1-alpha. More info: https://book.kubebuilder.io/plugins/available/autoupdate-v1-alpha

### Context
Kubebuilder currently offers a "Help to Upgrade" feature via the `kubebuilder alpha generate` command, but applying updates requires significant manual effort.

### Objective
Develop an opt-in mechanism to notify users and automate updates, reducing manual effort and ensuring alignment with the latest Kubebuilder versions.

### Goals
- Facilitate keeping repositories updated with minimal manual intervention.
- Provide automated notifications and updates inspired by Dependabot.
- Maintain compatibility with new Kubebuilder features, best practices, and bug fixes.

---


================================================
FILE: roadmap/roadmap_2026.md
================================================
# Kubebuilder Project Roadmap 2026

The main goals for 2026 are to promote adoption of the latest Kubebuilder versions and update automation, align tutorials and samples with best practices proposed by the [DeployImage plugin](https://kubebuilder.io/plugins/available/deploy-image-plugin-v1-alpha), improve documentation quality and consistency, explore how we can better leverage AI capabilities, and strengthen Kubebuilder as an API and plugin framework to encourage the creation and adoption of external plugins that extend and integrate with Kubebuilder.

## Promote and encourage adoption of the latest Kubebuilder versions and automation mechanisms to stay updated

**Status:** WIP

### Context
In 2025, we introduced the `kubebuilder alpha update` command and the AutoUpdate plugin to help users stay current with the latest Kubebuilder changes.
However, adoption of these features has been limited because many users are not aware of them or cannot update their projects easily to
the latest Kubebuilder versions and take advantage of these automation mechanisms.

### Objective
Ensure more projects use the latest Kubebuilder versions and adopt automation mechanisms to stay updated.

### Goals
- **Migration Documentation**:
  - (Status Done but looking for collaboration and follow up) Simplify and enhance migration documentation to guide users through updating their projects to the latest Kubebuilder versions. Highlight the available automation mechanisms and provide a generic guide to migrate from any version to the latest manually, so users can then adopt the automation mechanisms going forward.

- **Create Campaign to Promote Update Features**:
  - (Status TODO) Launch a campaign to raise awareness about the `kubebuilder alpha update` command and the AutoUpdate plugin, highlighting their benefits and encouraging adoption among users.
    - Similar to past campaigns where we created issues for public repos to help users become aware of critical changes (e.g., Kubernetes API deprecation from `v1beta1` to `v1`, and the migration away from `gcr.io/kubebuilder/kube-rbac-proxy`). See [discussion #3907](https://github.com/kubernetes-sigs/kubebuilder/discussions/3907).
    - Note: before running this campaign, ensure migration documentation is in place to support users through the update process.
    - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/5291)
---

## Enhance Webhooks CLI User Experience to support additional Webhook scaffolding without requiring `--force`

**Status:** TODO

### Objective
Enhance the webhooks implementation and user experience.

### Context
Currently, users can scaffold webhooks, but if they want to add additional webhook types for the same API they need to use `--force`.
This may overwrite existing customizations. The goal is to support iterative workflows where users can scaffold webhook type A and later add webhook type B
without requiring forced re-scaffolding. The `--force` flag should remain available.

### Goals and Needs
- **Scaffolding Multiple Webhooks**: Allow adding additional webhook types for the same API without requiring forced re-scaffolding.
  - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/4146)

---

## External plugins examples improvement and promotion

**Status:** TODO

### Objective
Enhance the examples to demonstrate usage of external plugins for end users and encourage the usage of Kubebuilder as an API and plugin framework.
Help projects create their own plugins to extend and integrate with Kubebuilder.

### Context
Kubebuilder supports external plugins, but we need clearer, maintained examples that show how to build, distribute, and use them in real projects.

### Goals and Needs
- **Make sampleexternalplugin a Valid Reference Implementation**
  - [GitHub Issue](https://github.com/kubernetes-sigs/kubebuilder/issues/4146)


================================================
FILE: test/check-docs-only.sh
================================================
#!/usr/bin/env bash

# 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.

# This script runs goreleaser using the build/.goreleaser.yml config.
# While it can be run locally, it is intended to be run by cloudbuild
# in the goreleaser/goreleaser image.

set -e

# If running in Github actions: this should be set to "github.base_ref".
: ${1?"the first argument must be set to a commit-ish reference"}

# Patterns to ignore.
declare -a DOC_PATTERNS
DOC_PATTERNS=(
  "(\.md)"
  "(\.MD)"
  "(\.png)"
  "(\.pdf)"
  "(netlify\.toml)"
  "(OWNERS)"
  "(OWNERS_ALIASES)"
  "(LICENSE)"
  "(docs/)"
)

if ! git diff --name-only $1 | grep -qvE "$(IFS="|"; echo "${DOC_PATTERNS[*]}")"; then
  echo "true"
  exit 0
fi


================================================
FILE: test/check-license.sh
================================================
#!/bin/bash

# Copyright 2021 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

set -o errexit
set -o nounset
set -o pipefail

source $(dirname "$0")/common.sh

echo "Checking for license header..."
#TODO: See if we can improve the Bollerplate logic for the Hack/License to allow scaffold the licenses
#using the comment prefix # for yaml files.
allfiles=$(listFiles | grep -v -e './internal/bindata/...' -e '.devcontainer/post-install.sh' -e '.github/*')
licRes=""
for file in $allfiles; do
  if [[ -f "$file" && "$(file --mime-type -b "$file")" == text/* ]]; then
    # Read the first few lines but skip build tags for Go files
    # Strip up to 3 lines starting with //go:build or // +build
    stripped=$(head -n 30 "$file" \
      | sed '/^\/\/go:build\|^\/\/ +build/d' \
      | sed '/^\s*$/d' \
      | head -n 10)
    if ! echo "$stripped" | grep -Eq "(Copyright|generated|GENERATED|Licensed)" ; then
      licRes="${licRes}\n  ${file}"
    fi
  fi
done
if [ -n "${licRes}" ]; then
  echo -e "license header checking failed:\n${licRes}"
  exit 255
fi


================================================
FILE: test/check_spaces.sh
================================================
#!/usr/bin/env bash

# Copyright 2024 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.

function validate_docs_trailing_spaces {
  if find . -type f -name "*.md" -exec grep -Hn '[[:space:]]$' {} +; then
      echo "Trailing spaces were found in docs files"
      exit 1
  fi

}

validate_docs_trailing_spaces


================================================
FILE: test/common.sh
================================================
#!/usr/bin/env bash

# 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.

# Not every exact cluster version has an equal tools version, and visa versa.
# This function returns the exact tools version for a k8s version based on its minor.
function convert_to_tools_ver {
  local k8s_ver=${1:?"k8s version must be set to arg 1"}
  local maj_min=$(echo $k8s_ver | grep -oE '^[0-9]+\.[0-9]+')
  case $maj_min in
  # 1.14-1.19 work with the 1.19 server bins and kubectl.
  "1.14"|"1.15"|"1.16"|"1.17"|"1.18"|"1.19") echo "1.19.2";;
  # Tests in 1.20 and 1.21 with their counterpart version's apiserver.
  "1.20"|"1.21") echo "1.21.5";;
  "1.22") echo "1.22.1";;
  "1.23") echo "1.23.3";;
  "1.24") echo "1.24.1";;
  "1.25") echo "1.25.0";;
  "1.26") echo "1.26.0";;
  "1.27") echo "1.27.1";;
  "1.28") echo "1.28.3";;
  "1.29") echo "1.29.0";;
  "1.30") echo "1.30.0";;
  "1.31") echo "1.31.0";;
  "1.32") echo "1.32.0";;
  "1.33") echo "1.33.0";;
  "1.34") echo "1.34.0";;
  "1.35") echo "1.35.0";;
  *)
    echo "k8s version $k8s_ver not supported"
    exit 1
  esac
}

set -o errexit
set -o nounset
set -o pipefail

# Force a static development version for ALL scripts that source common.sh.
# This ensures PROJECT files in both /testdata and /docs remain consistent.
export KUBEBUILDER_TEST_VERSION="(devel)"

# Enable tracing in this script off by setting the TRACE variable in your
# environment to any value:
#
# $ TRACE=1 test.sh
TRACE=${TRACE:-""}
if [ -n "$TRACE" ]; then
  set -x
fi

export KIND_K8S_VERSION="${KIND_K8S_VERSION:-"v1.35.0"}"
tools_k8s_version=$(convert_to_tools_ver "${KIND_K8S_VERSION#v*}")
kind_version=0.29.0
goarch=amd64

if [[ "$OSTYPE" == "linux-gnu" ]]; then
  goos="linux"
elif [[ "$OSTYPE" == "darwin"* ]]; then
  goos="darwin"
#elif [[ "$OS" == "Windows_NT" ]]; then
#  goos="windows"
else
  echo "OS '$OSTYPE' not supported. Aborting." >&2
  exit 1
fi

# Turn colors in this script off by setting the NO_COLOR variable in your
# environment to any value:
#
# $ NO_COLOR=1 test.sh
NO_COLOR=${NO_COLOR:-""}
if [ -z "$NO_COLOR" ]; then
  header=$'\e[1;33m'
  reset=$'\e[0m'
else
  header=''
  reset=''
fi

function header_text {
  echo "$header$*$reset"
}

# Certain tools are installed to GOBIN.
export PATH="$(go env GOPATH)/bin:${PATH}"

# Kubebuilder's bin path should be the last added to PATH such that it is preferred.
tmp_root=/tmp
kb_root_dir=$tmp_root/kubebuilder
mkdir -p "$kb_root_dir"
export PATH="${kb_root_dir}/bin:${PATH}"

# Skip fetching and untaring the tools by setting the SKIP_FETCH_TOOLS variable
# in your environment to any value:
#
# $ SKIP_FETCH_TOOLS=1 ./test.sh
#
# If you skip fetching tools, this script will use the tools already on your
# machine, but rebuild the kubebuilder and kubebuilder-bin binaries.
SKIP_FETCH_TOOLS=${SKIP_FETCH_TOOLS:-""}

# Build kubebuilder
function build_kb {
  header_text "Building kubebuilder"

  go build -o "${kb_root_dir}/bin/kubebuilder"
  kb="${kb_root_dir}/bin/kubebuilder"
}

# Fetch k8s API tools and manage them globally with setup-envtest.
function fetch_tools {
  if ! is_installed setup-envtest; then
    header_text "Installing setup-envtest to $(go env GOPATH)/bin"

    # TODO: Current workaround for setup-envtest compatibility
    # Due to past instances where controller-runtime maintainers released
    # versions without corresponding branches, directly relying on branches
    # poses a risk of breaking the Kubebuilder chain. Such practices may
    # change over time, potentially leading to compatibility issues. This
    # approach, although not ideal, remains the best solution for ensuring
    # compatibility with controller-runtime releases as of now. For more
    # details on the quest for a more robust solution, refer to the issue
    # raised in the controller-runtime repository: https://github.com/kubernetes-sigs/controller-runtime/issues/2744
    go install sigs.k8s.io/controller-runtime/tools/setup-envtest@release-0.22
  fi

  if [ -z "$SKIP_FETCH_TOOLS" ]; then
    header_text "Installing e2e tools with setup-envtest"

    setup-envtest use $tools_k8s_version
  fi

  # Export KUBEBUILDER_ASSETS.
  eval $(setup-envtest use -i -p env $tools_k8s_version)
  # Downloaded tools should be used instead of counterparts present in the environment.
  export PATH="${KUBEBUILDER_ASSETS}:${PATH}"
}

# Installing kind in a temporal dir if no previously installed to GOBIN.
function install_kind {
  if ! is_installed kind ; then
    header_text "Installing kind to $(go env GOPATH)/bin"

    go install sigs.k8s.io/kind@v$kind_version
  fi
}

# Check if a program is previously installed
function is_installed {
  if command -v $1 &>/dev/null; then
    return 0
  fi
  return 1
}

function listPkgDirs() {
	go list -f '{{.Dir}}' ./... | grep -v generated
}

#Lists all go files
function listFiles() {
	# pipeline is much faster than for loop
	listPkgDirs | xargs -I {} find {} \( -name '*.go' -o -name '*.sh' \)  | grep -v generated
}


================================================
FILE: test/e2e/all/e2e_suite_test.go
================================================
/*
Copyright 2025 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 all

import (
	"fmt"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

// Run unified e2e tests using the Ginkgo runner.
// This consolidates all plugin tests (v4, helm, deployimage) into a single suite
// to share infrastructure setup (cert-manager, Prometheus) and reduce overhead.
func TestE2E(t *testing.T) {
	RegisterFailHandler(Fail)
	_, _ = fmt.Fprintf(GinkgoWriter, "Starting unified Kubebuilder e2e suite (all plugins)\n")
	RunSpecs(t, "Kubebuilder Unified E2E Suite")
}

// BeforeSuite runs once before all specs to set up shared infrastructure.
// This is run ONCE instead of 3 times (once per plugin), significantly reducing test time.
var _ = BeforeSuite(func() {
	var err error

	_, _ = fmt.Fprintf(GinkgoWriter, "\n=== Setting up shared test infrastructure ===\n")

	kbc, err := utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on")
	Expect(err).NotTo(HaveOccurred())
	Expect(kbc.Prepare()).To(Succeed())

	By("installing cert-manager bundle (shared across all tests)")
	Expect(kbc.InstallCertManager()).To(Succeed())

	By("installing Prometheus operator (shared across all tests)")
	Expect(kbc.InstallPrometheusOperManager()).To(Succeed())

	_, _ = fmt.Fprintf(GinkgoWriter, "=== Shared infrastructure ready ===\n\n")
})

// AfterSuite runs once after all specs to clean up shared infrastructure.
var _ = AfterSuite(func() {
	_, _ = fmt.Fprintf(GinkgoWriter, "\n=== Cleaning up shared test infrastructure ===\n")

	kbc, err := utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on")
	Expect(err).NotTo(HaveOccurred())
	Expect(kbc.Prepare()).To(Succeed())

	By("uninstalling Prometheus operator")
	kbc.UninstallPrometheusOperManager()

	By("uninstalling cert-manager bundle")
	kbc.UninstallCertManager()

	_, _ = fmt.Fprintf(GinkgoWriter, "=== Cleanup complete ===\n")
})


================================================
FILE: test/e2e/all/plugin_deployimage_test.go
================================================
/*
Copyright 2022 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 all

import (
	"fmt"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

// Test specs for deploy-image plugin
var _ = Describe("kubebuilder", func() {
	Context("deploy image plugin", func() {
		var kbc *utils.TestContext

		BeforeEach(func() {
			var err error
			kbc, err = utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on")
			Expect(err).NotTo(HaveOccurred())
			Expect(kbc.Prepare()).To(Succeed())
		})

		AfterEach(func() {
			By("clean up API objects created during the test")
			Expect(kbc.Make("undeploy")).To(Succeed())

			By("removing controller image and working dir")
			kbc.Destroy()
		})

		It("should generate a runnable project with deploy-image/v1-alpha options ", func() {
			generateDeployImageWithOptions(kbc)
			runDeployImageTests(kbc)
		})

		It("should generate a runnable project with deploy-image/v1-alpha without options ", func() {
			generateDeployImage(kbc)
			runDeployImageTests(kbc)
		})
	})
})

// generateDeployImageWithOptions implements a go/v4 plugin project and scaffold an API using the image options
func generateDeployImageWithOptions(kbc *utils.TestContext) {
	initDeployImageProject(kbc)

	By("creating API definition with deploy-image/v1-alpha plugin with options")
	err := kbc.CreateAPI(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--plugins", "deploy-image/v1-alpha",
		"--image", "memcached:1.6.26-alpine3.19",
		"--image-container-port", "11211",
		"--image-container-command", "memcached,--memory-limit=64,-o,modern,-v",
		"--run-as-user", "1001",
		"--make=false",
		"--manifests=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to create API definition with deploy-image/v1-alpha")
}

// generateDeployImage implements a go/v4 plugin project and scaffold an API using the deploy image plugin
func generateDeployImage(kbc *utils.TestContext) {
	initDeployImageProject(kbc)

	By("creating API definition with deploy-image/v1-alpha plugin")
	err := kbc.CreateAPI(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--plugins", "deploy-image/v1-alpha",
		"--image", "busybox:1.36.1",
		"--run-as-user", "1001",
		"--make=false",
		"--manifests=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to create API definition")
}

func initDeployImageProject(kbc *utils.TestContext) {
	By("initializing a project")
	err := kbc.Init(
		"--plugins", "go/v4",
		"--project-version", "3",
		"--domain", kbc.Domain,
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to initialize project")
}

// runDeployImageTests runs a set of e2e tests for a scaffolded deploy-image project.
func runDeployImageTests(kbc *utils.TestContext) {
	var controllerPodName string
	var err error

	SetDefaultEventuallyPollingInterval(time.Second)
	SetDefaultEventuallyTimeout(time.Minute)

	By("updating the go.mod")
	Expect(kbc.Tidy()).To(Succeed())

	By("run make manifests")
	Expect(kbc.Make("manifests")).To(Succeed())

	By("run make generate")
	Expect(kbc.Make("generate")).To(Succeed())

	By("run make all")
	Expect(kbc.Make("all")).To(Succeed())

	By("run make install")
	Expect(kbc.Make("install")).To(Succeed())

	By("building the controller image")
	Expect(kbc.Make("docker-build", "IMG="+kbc.ImageName)).To(Succeed())

	By("loading the controller docker image into the kind cluster")
	Expect(kbc.LoadImageToKindCluster()).To(Succeed())

	By("deploying the controller-manager")
	cmd := exec.Command("make", "deploy", "IMG="+kbc.ImageName)
	out, err := kbc.Run(cmd)
	Expect(err).NotTo(HaveOccurred())
	Expect(string(out)).NotTo(ContainSubstring("Warning: would violate PodSecurity"))

	By("validating that the controller-manager pod is running as expected")
	verifyControllerUp := func(g Gomega) {
		// Get pod name
		var podOutput string
		podOutput, err = kbc.Kubectl.Get(
			true,
			"pods", "-l", "control-plane=controller-manager",
			"-o", "go-template={{ range .items }}{{ if not .metadata.deletionTimestamp }}{{ .metadata.name }}"+
				"{{ \"\\n\" }}{{ end }}{{ end }}")
		g.Expect(err).NotTo(HaveOccurred())
		podNames := util.GetNonEmptyLines(podOutput)
		g.Expect(podNames).To(HaveLen(1), "wrong number of controller-manager pods")
		controllerPodName = podNames[0]
		g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))

		// Validate pod status
		g.Expect(kbc.Kubectl.Get(true, "pods", controllerPodName, "-o", "jsonpath={.status.phase}")).
			To(Equal("Running"), "incorrect controller pod status")
	}
	defer func() {
		out, errDescribe := kbc.Kubectl.CommandInNamespace("describe", "all")
		Expect(errDescribe).NotTo(HaveOccurred())
		_, _ = fmt.Fprintln(GinkgoWriter, out)
	}()
	Eventually(verifyControllerUp).Should(Succeed())

	By("creating an instance of the CR")
	sampleFile := filepath.Join("config", "samples",
		fmt.Sprintf("%s_%s_%s.yaml", kbc.Group, kbc.Version, strings.ToLower(kbc.Kind)))

	sampleFilePath, err := filepath.Abs(filepath.Join(fmt.Sprintf("e2e-%s", kbc.TestSuffix), sampleFile))
	Expect(err).To(Not(HaveOccurred()))

	Eventually(func(g Gomega) {
		g.Expect(kbc.Kubectl.Apply(true, "-f", sampleFilePath)).Error().NotTo(HaveOccurred())
	}).Should(Succeed())

	By("validating that pod(s) status.phase=Running")
	verifyMemcachedPodStatus := func(g Gomega) {
		g.Expect(kbc.Kubectl.Get(true, "pods", "-l",
			fmt.Sprintf("app.kubernetes.io/name=e2e-%s", kbc.TestSuffix),
			"-o", "jsonpath={.items[*].status}",
		)).To(ContainSubstring("\"phase\":\"Running\""))
	}
	Eventually(verifyMemcachedPodStatus).Should(Succeed())

	By("validating that the status of the custom resource created is updated or not")
	verifyAvailableStatus := func(g Gomega) {
		g.Expect(kbc.Kubectl.Get(true, strings.ToLower(kbc.Kind),
			strings.ToLower(kbc.Kind)+"-sample",
			"-o", "jsonpath={.status.conditions}")).To(ContainSubstring("Available"),
			`status condition with type "Available" should be set`)
	}
	Eventually(verifyAvailableStatus).Should(Succeed())

	By("validating the finalizer")
	Eventually(func(g Gomega) {
		g.Expect(kbc.Kubectl.Delete(true, "-f", sampleFilePath)).Error().NotTo(HaveOccurred())
	}).Should(Succeed())

	Eventually(func(g Gomega) {
		g.Expect(kbc.Kubectl.Get(true, "events", "--field-selector=type=Warning",
			"-o", "jsonpath={.items[*].message}",
		)).To(ContainSubstring("is being deleted from the namespace"))
	}).Should(Succeed())
}


================================================
FILE: test/e2e/all/plugin_helm_test.go
================================================
/*
Copyright 2025 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 all

import (
	"fmt"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/internal/helpers"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

// Test specs for helm/v2-alpha plugin
var _ = Describe("kubebuilder", func() {
	Context("plugin helm/v2-alpha", func() {
		var kbc *utils.TestContext

		BeforeEach(func() {
			var err error
			kbc, err = utils.NewTestContext(pluginutil.KubebuilderBinName, "GO111MODULE=on")
			Expect(err).NotTo(HaveOccurred())
			Expect(kbc.Prepare()).To(Succeed())

			By("installing Helm binary for chart operations")
			Expect(kbc.InstallHelm()).To(Succeed())
		})

		AfterEach(func() {
			By("removing restricted namespace label")
			_ = kbc.RemoveNamespaceLabelToEnforceRestricted()

			By("uninstalling Helm Release (if installed)")
			_ = kbc.UninstallHelmRelease()

			By("cleaning up CRDs that were preserved by crd.keep=true")
			domainSuffix := fmt.Sprintf(".example.com%s", kbc.TestSuffix)
			listCmd := exec.Command("kubectl", "get", "crds", "-o", "name")
			if output, err := kbc.Run(listCmd); err == nil {
				for crdName := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") {
					if crdName != "" && strings.Contains(crdName, domainSuffix) {
						deleteCmd := exec.Command("kubectl", "delete", crdName, "--ignore-not-found")
						_, _ = kbc.Run(deleteCmd)
					}
				}
			}

			By("removing controller image and working dir")
			kbc.Destroy()
		})

		It("should generate a runnable project using webhooks and installed with the HelmChart", func() {
			helpers.GenerateV4(kbc)

			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodHelm,
			})

			By("upgrading the release to 3 replicas and verifying " +
				"manager.replicas is respected (leader election ensures only one active)")
			Expect(kbc.HelmUpgradeReleaseWithReplicas(3)).To(Succeed())
			deploymentName := fmt.Sprintf("e2e-%s-controller-manager", kbc.TestSuffix)
			Eventually(func(g Gomega) {
				out, err := kbc.Kubectl.Get(
					true,
					"deployment", deploymentName,
					"-o", "jsonpath={.spec.replicas}",
				)
				g.Expect(err).NotTo(HaveOccurred())
				replicas, atoiErr := strconv.Atoi(strings.TrimSpace(out))
				g.Expect(atoiErr).NotTo(HaveOccurred(), "replicas field is not an integer")
				g.Expect(replicas).To(Equal(3), "expected deployment spec.replicas to be 3 after helm upgrade")
			}, helpers.DefaultTimeout, 30*time.Second).Should(Succeed())
		})

		It("should generate a runnable project without metrics exposed", func() {
			helpers.GenerateV4WithoutMetrics(kbc)

			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         false,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodHelm,
			})
		})

		It("should generate a runnable project with metrics protected by network policies", func() {
			helpers.GenerateV4WithNetworkPoliciesWithoutWebhooks(kbc)

			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         false,
				HasMetrics:         true,
				HasNetworkPolicies: true,
				InstallMethod:      helpers.InstallMethodHelm,
			})
		})

		It("should generate a runnable project with webhooks and metrics protected by network policies", func() {
			helpers.GenerateV4WithNetworkPolicies(kbc)

			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: true,
				InstallMethod:      helpers.InstallMethodHelm,
			})
		})

		It("should generate a runnable project with the manager running as restricted and without webhooks", func() {
			helpers.GenerateV4WithoutWebhooks(kbc)

			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         false,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodHelm,
			})
		})

		It("should work with Helm chart customizations (fullnameOverride and cert-manager)", func() {
			By("generating a full-featured project with webhooks, metrics, and conversion webhooks")
			helpers.GenerateV4(kbc)

			By("building installer and generating helm chart")
			Expect(kbc.Make("build-installer")).To(Succeed())
			err := kbc.EditHelmPlugin()
			Expect(err).NotTo(HaveOccurred())

			By("customizing chart name via fullnameOverride to validate runtime behavior")
			valuesPath := filepath.Join(kbc.Dir, "dist", "chart", "values.yaml")
			err = pluginutil.ReplaceInFile(valuesPath,
				`# fullnameOverride: ""`,
				`fullnameOverride: "custom-operator"`)
			Expect(err).NotTo(HaveOccurred())

			By("deploying with custom chart name - validates cert-manager and all resources work correctly")
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:           true,
				HasMetrics:           true,
				HasNetworkPolicies:   false,
				InstallMethod:        helpers.InstallMethodHelm,
				HelmFullnameOverride: "custom-operator",
				SkipChartGeneration:  true, // Chart already generated and customized above
			})
		})

		It("should generate a namespeced runnable project using webhooks and installed with the HelmChart", func() {
			helpers.GenerateV4Namespaced(kbc)

			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				IsNamespaced:       true,
				InstallMethod:      helpers.InstallMethodHelm,
			})
		})

		It("should delete CRDs on helm uninstall when crd.keep=false", func() {
			By("generating a project with webhooks")
			helpers.GenerateV4(kbc)

			By("building installer and generating helm chart")
			Expect(kbc.Make("build-installer")).To(Succeed())
			err := kbc.EditHelmPlugin()
			Expect(err).NotTo(HaveOccurred())

			By("installing helm chart with crd.keep=false")
			Expect(kbc.HelmInstallReleaseWithOptions(false)).To(Succeed())

			By("verifying CRDs exist after install")
			domainSuffix := fmt.Sprintf(".example.com%s", kbc.TestSuffix)
			verifyCRDsExist := func(g Gomega) {
				listCmd := exec.Command("kubectl", "get", "crds", "-o", "name")
				output, err := kbc.Run(listCmd)
				g.Expect(err).NotTo(HaveOccurred(), "failed to list CRDs")
				g.Expect(string(output)).To(ContainSubstring(domainSuffix),
					"expected CRDs matching domain suffix %s to exist", domainSuffix)
			}
			verifyCRDsExist(Default)

			By("uninstalling helm release")
			Expect(kbc.UninstallHelmRelease()).To(Succeed())

			By("verifying CRDs are deleted after uninstall (crd.keep=false)")
			verifyCRDsDeleted := func(g Gomega) {
				listCmd := exec.Command("kubectl", "get", "crds", "-o", "name")
				output, err := kbc.Run(listCmd)
				if err != nil {
					// If we can't list CRDs, assume they're gone
					return
				}
				for crdName := range strings.SplitSeq(strings.TrimSpace(string(output)), "\n") {
					g.Expect(crdName).NotTo(ContainSubstring(domainSuffix),
						"CRD %s still exists but should have been deleted", crdName)
				}
			}
			Eventually(verifyCRDsDeleted, helpers.DefaultTimeout, 30*time.Second).Should(Succeed())
		})
	})
})


================================================
FILE: test/e2e/all/plugin_v4_test.go
================================================
/*
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package all

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/internal/helpers"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

// Test specs for go/v4 plugin
var _ = Describe("kubebuilder", func() {
	Context("plugin go/v4", func() {
		var kbc *utils.TestContext

		BeforeEach(func() {
			var err error
			kbc, err = utils.NewTestContext(util.KubebuilderBinName, "GO111MODULE=on")
			Expect(err).NotTo(HaveOccurred())
			Expect(kbc.Prepare()).To(Succeed())
		})

		AfterEach(func() {
			By("removing restricted namespace label")
			_ = kbc.RemoveNamespaceLabelToEnforceRestricted()

			By("undeploy the project")
			_ = kbc.Make("undeploy")

			By("uninstalling the project")
			_ = kbc.Make("uninstall")

			By("removing controller image and working dir")
			kbc.Destroy()
		})

		It("should generate a runnable project", func() {
			helpers.GenerateV4(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodKustomize,
			})
		})

		It("should generate a runnable project with the Installer", func() {
			helpers.GenerateV4(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodInstaller,
			})
		})

		It("should generate a runnable project without metrics exposed", func() {
			helpers.GenerateV4WithoutMetrics(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         false,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodKustomize,
			})
		})

		It("should generate a runnable project with metrics protected by network policies", func() {
			helpers.GenerateV4WithNetworkPoliciesWithoutWebhooks(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         false,
				HasMetrics:         true,
				HasNetworkPolicies: true,
				InstallMethod:      helpers.InstallMethodKustomize,
			})
		})

		It("should generate a runnable project with webhooks and metrics protected by network policies", func() {
			helpers.GenerateV4WithNetworkPolicies(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: true,
				InstallMethod:      helpers.InstallMethodKustomize,
			})
		})

		It("should generate a runnable project with the manager running "+
			"as restricted and without webhooks", func() {
			helpers.GenerateV4WithoutWebhooks(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         false,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodKustomize,
			})
		})

		It("should generate a runnable project with custom webhook paths", func() {
			helpers.GenerateV4WithCustomWebhookPath(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				InstallMethod:      helpers.InstallMethodKustomize,
			})
		})

		It("should generate a runnable project", func() {
			helpers.GenerateV4Namespaced(kbc)
			helpers.Run(kbc, helpers.RunOptions{
				HasWebhook:         true,
				HasMetrics:         true,
				HasNetworkPolicies: false,
				IsNamespaced:       true,
				InstallMethod:      helpers.InstallMethodKustomize,
			})
		})
	})
})


================================================
FILE: test/e2e/ci.sh
================================================
#!/usr/bin/env bash

# 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.

source "$(dirname "$0")/../common.sh"
source "$(dirname "$0")/setup.sh"

export KIND_CLUSTER="kind"
create_cluster ${KIND_K8S_VERSION}
trap delete_cluster EXIT

test_cluster -v -ginkgo.vv


================================================
FILE: test/e2e/internal/helpers/generate_v4.go
================================================
/*
Copyright 2020 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package helpers

import (
	"fmt"
	"os"
	"path/filepath"
	"strings"

	. "github.com/onsi/ginkgo/v2" //nolint:staticcheck
	. "github.com/onsi/gomega"    //nolint:staticcheck

	pluginutil "sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

// GenerateV4 implements a go/v4 plugin project defined by a TestContext.
func GenerateV4(kbc *utils.TestContext) {
	initingTheProject(kbc)
	creatingAPI(kbc)

	By("scaffolding mutating and validating webhooks")
	err := kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--defaulting",
		"--programmatic-validation",
		"--make=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to scaffolding mutating webhook")

	By("implementing the mutating and validating webhooks")
	webhookFilePath := filepath.Join(
		kbc.Dir, "internal/webhook", kbc.Version,
		fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind)))
	err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
	Expect(err).NotTo(HaveOccurred(), "Failed to implement webhooks")

	scaffoldConversionWebhook(kbc)

	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../prometheus", "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "prometheus", "kustomization.yaml"),
		monitorTLSPatch, "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsCertPatch, "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsCertReplaces, "#")).To(Succeed())
}

// GenerateV4WithoutMetrics implements a go/v4 plugin project defined by a TestContext.
func GenerateV4WithoutMetrics(kbc *utils.TestContext) {
	initingTheProject(kbc)
	creatingAPI(kbc)

	By("scaffolding mutating and validating webhooks")
	err := kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--defaulting",
		"--programmatic-validation",
		"--make=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to scaffolding mutating webhook")

	By("implementing the mutating and validating webhooks")
	webhookFilePath := filepath.Join(
		kbc.Dir, "internal/webhook", kbc.Version,
		fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind)))
	err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
	Expect(err).NotTo(HaveOccurred(), "Failed to implement webhooks")

	scaffoldConversionWebhook(kbc)
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../prometheus", "#")).To(Succeed())
	// Disable metrics
	ExpectWithOffset(1, pluginutil.CommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"- metrics_service.yaml", "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.CommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsTarget, "#")).To(Succeed())
}

// GenerateV4WithoutMetrics implements a go/v4 plugin project defined by a TestContext.
func GenerateV4WithNetworkPoliciesWithoutWebhooks(kbc *utils.TestContext) {
	initingTheProject(kbc)
	creatingAPI(kbc)

	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../prometheus", "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsTarget, "#")).To(Succeed())
	By("uncomment kustomization.yaml to enable network policy")
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../network-policy", "#")).To(Succeed())
}

// GenerateV4WithNetworkPolicies implements a go/v4 plugin project defined by a TestContext.
func GenerateV4WithNetworkPolicies(kbc *utils.TestContext) {
	initingTheProject(kbc)
	creatingAPI(kbc)

	By("scaffolding mutating and validating webhooks")
	err := kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--defaulting",
		"--programmatic-validation",
		"--make=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to scaffolding mutating webhook")

	By("implementing the mutating and validating webhooks")
	webhookFilePath := filepath.Join(
		kbc.Dir, "internal/webhook", kbc.Version,
		fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind)))
	err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
	Expect(err).NotTo(HaveOccurred(), "Failed to implement webhooks")

	scaffoldConversionWebhook(kbc)
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../prometheus", "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsTarget, "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsCertPatch, "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsCertReplaces, "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "prometheus", "kustomization.yaml"),
		monitorTLSPatch, "#")).To(Succeed())

	By("uncomment kustomization.yaml to enable network policy")
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../network-policy", "#")).To(Succeed())
}

// GenerateV4WithoutWebhooks implements a go/v4 plugin with APIs and enable Prometheus and CertManager
func GenerateV4WithoutWebhooks(kbc *utils.TestContext) {
	initingTheProject(kbc)
	creatingAPI(kbc)

	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../prometheus", "#")).To(Succeed())
}

// GenerateV4WithCustomWebhookPath tests webhooks with custom paths
func GenerateV4WithCustomWebhookPath(kbc *utils.TestContext) {
	initingTheProject(kbc)
	creatingAPI(kbc)

	By("scaffolding both defaulting and validation webhooks with different custom paths")
	err := kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--defaulting",
		"--programmatic-validation",
		"--defaulting-path=/custom-mutate-path",
		"--validation-path=/custom-validate-path",
		"--make=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to scaffold webhooks with custom paths")

	By("verifying custom webhook paths in generated webhook file")
	webhookFilePath := filepath.Join(
		kbc.Dir, "internal/webhook", kbc.Version,
		fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind)))

	// Read the webhook file and check if both custom paths are present
	content, err := os.ReadFile(webhookFilePath)
	Expect(err).NotTo(HaveOccurred(), "Failed to read webhook file")
	Expect(string(content)).To(ContainSubstring("path=/custom-mutate-path"),
		"Webhook file should contain custom defaulting path")
	Expect(string(content)).To(ContainSubstring("path=/custom-validate-path"),
		"Webhook file should contain custom validation path")

	By("implementing the webhooks")
	err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
	Expect(err).NotTo(HaveOccurred(), "Failed to implement webhook")

	scaffoldConversionWebhook(kbc)
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../prometheus", "#")).To(Succeed())

	By("verifying that --defaulting-path requires --defaulting flag")
	err = kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", "InvalidTest",
		"--defaulting-path=/invalid-path",
		"--make=false",
	)
	Expect(err).To(HaveOccurred(), "Should fail when --defaulting-path is used without --defaulting")
	Expect(err.Error()).To(ContainSubstring("--defaulting-path can only be used with --defaulting"))

	By("verifying that --validation-path requires --programmatic-validation flag")
	err = kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", "InvalidTest",
		"--validation-path=/invalid-path",
		"--make=false",
	)
	Expect(err).To(HaveOccurred(), "Should fail when --validation-path is used without --programmatic-validation")
	Expect(err.Error()).To(ContainSubstring("--validation-path can only be used with --programmatic-validation"))
}

func creatingAPI(kbc *utils.TestContext) {
	By("creating API definition")
	err := kbc.CreateAPI(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--namespaced",
		"--resource",
		"--controller",
		"--make=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to create API")

	By("implementing the API")
	ExpectWithOffset(1, pluginutil.InsertCode(
		filepath.Join(kbc.Dir, "api", kbc.Version, fmt.Sprintf("%s_types.go", strings.ToLower(kbc.Kind))),
		fmt.Sprintf(`type %sSpec struct {
`, kbc.Kind),
		`	// +optional
Count int `+"`"+`json:"count,omitempty"`+"`"+`
`)).Should(Succeed())
}

func initingTheProject(kbc *utils.TestContext) {
	By("initializing a project")
	err := kbc.Init(
		"--plugins", "go/v4",
		"--project-version", "3",
		"--domain", kbc.Domain,
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to initialize project")
}

func initingNamespacedProject(kbc *utils.TestContext) {
	By("initializing a namespace-scoped project")
	err := kbc.Init(
		"--plugins", "go/v4",
		"--project-version", "3",
		"--domain", kbc.Domain,
		"--namespaced",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to initialize namespace-scoped project")
}

const metricsTarget = `- path: manager_metrics_patch.yaml
  target:
    kind: Deployment`

// scaffoldConversionWebhook sets up conversion webhooks for testing the ConversionTest API
func scaffoldConversionWebhook(kbc *utils.TestContext) {
	By("scaffolding conversion webhooks for testing ConversionTest v1 to v2 conversion")

	// Create API for v1 (hub) with conversion enabled
	err := kbc.CreateAPI(
		"--group", kbc.Group,
		"--version", "v1",
		"--kind", "ConversionTest",
		"--controller=true",
		"--resource=true",
		"--make=false",
	)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create v1 API for conversion testing")

	// Create API for v2 (spoke) without a controller
	err = kbc.CreateAPI(
		"--group", kbc.Group,
		"--version", "v2",
		"--kind", "ConversionTest",
		"--controller=false",
		"--resource=true",
		"--make=false",
	)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create v2 API for conversion testing")

	// Create the conversion webhook for v1
	By("setting up the conversion webhook for v1")
	err = kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", "v1",
		"--kind", "ConversionTest",
		"--conversion",
		"--spoke", "v2",
		"--make=false",
	)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "failed to create conversion webhook for v1")

	// Insert Size field in v1
	By("implementing the size spec in v1")
	ExpectWithOffset(1, pluginutil.InsertCode(
		filepath.Join(kbc.Dir, "api", "v1", "conversiontest_types.go"),
		"Foo *string `json:\"foo,omitempty\"`",
		"\n\tSize int `json:\"size,omitempty\"` // Number of desired instances",
	)).NotTo(HaveOccurred(), "failed to add size spec to conversiontest_types v1")

	// Insert Replicas field in v2
	By("implementing the replicas spec in v2")
	ExpectWithOffset(1, pluginutil.InsertCode(
		filepath.Join(kbc.Dir, "api", "v2", "conversiontest_types.go"),
		"Foo *string `json:\"foo,omitempty\"`",
		"\n\tReplicas int `json:\"replicas,omitempty\"` // Number of replicas",
	)).NotTo(HaveOccurred(), "failed to add replicas spec to conversiontest_conversion.go v2")

	err = pluginutil.ReplaceInFile(filepath.Join(kbc.Dir, "api/v2/conversiontest_conversion.go"),
		"// dst.Spec.Size = src.Spec.Replicas",
		"dst.Spec.Size = src.Spec.Replicas")
	Expect(err).NotTo(HaveOccurred(), "failed to implement conversion logic from v1 to v2")

	err = pluginutil.ReplaceInFile(filepath.Join(kbc.Dir, "api/v2/conversiontest_conversion.go"),
		"// dst.Spec.Replicas = src.Spec.Size",
		"dst.Spec.Replicas = src.Spec.Size")
	Expect(err).NotTo(HaveOccurred(), "failed to implement conversion logic from v2 to v1")
}

const monitorTLSPatch = `#patches:
#  - path: monitor_tls_patch.yaml
#    target:
#      kind: ServiceMonitor`

const metricsCertPatch = `#- path: cert_metrics_manager_patch.yaml
#  target:
#    kind: Deployment`

const metricsCertReplaces = `# - source: # Uncomment the following block to enable certificates for metrics
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.name
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 0
#         create: true
#     - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 0
#         create: true

# - source:
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.namespace
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 1
#         create: true
#     - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 1
#         create: true`

// GenerateV4Namespaced implements a go/v4 plugin namespace-scoped project defined by a TestContext.
func GenerateV4Namespaced(kbc *utils.TestContext) {
	initingNamespacedProject(kbc)
	creatingAPI(kbc)

	By("scaffolding mutating and validating webhooks")
	err := kbc.CreateWebhook(
		"--group", kbc.Group,
		"--version", kbc.Version,
		"--kind", kbc.Kind,
		"--defaulting",
		"--programmatic-validation",
		"--make=false",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to scaffolding mutating webhook")

	By("implementing the mutating and validating webhooks")
	webhookFilePath := filepath.Join(
		kbc.Dir, "internal/webhook", kbc.Version,
		fmt.Sprintf("%s_webhook.go", strings.ToLower(kbc.Kind)))
	err = utils.ImplementWebhooks(webhookFilePath, strings.ToLower(kbc.Kind))
	Expect(err).NotTo(HaveOccurred(), "Failed to implement webhooks")

	scaffoldConversionWebhook(kbc)

	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		"#- ../prometheus", "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "prometheus", "kustomization.yaml"),
		monitorTLSPatch, "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsCertPatch, "#")).To(Succeed())
	ExpectWithOffset(1, pluginutil.UncommentCode(
		filepath.Join(kbc.Dir, "config", "default", "kustomization.yaml"),
		metricsCertReplaces, "#")).To(Succeed())
}


================================================
FILE: test/e2e/internal/helpers/plugin_test_helper.go
================================================
/*
Copyright 2024 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 helpers

import (
	"fmt"
	"os/exec"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2" //nolint:staticcheck
	. "github.com/onsi/gomega"    //nolint:staticcheck

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

const (
	// DefaultTimeout is the default timeout for Eventually checks
	DefaultTimeout = 3 * time.Minute
	// DefaultPollingInterval is the default polling interval for Eventually checks
	DefaultPollingInterval = time.Second

	// defaultTimeout is the default timeout for Eventually checks (package-internal alias)
	defaultTimeout = DefaultTimeout
	// defaultPollingInterval is the default polling interval for Eventually checks (package-internal alias)
	defaultPollingInterval = DefaultPollingInterval
)

// InstallMethod defines how the project will be deployed
type InstallMethod string

const (
	// InstallMethodKustomize uses `make deploy` (default)
	InstallMethodKustomize InstallMethod = "kustomize"
	// InstallMethodInstaller uses `build-installer` and applies dist/install.yaml
	InstallMethodInstaller InstallMethod = "installer"
	// InstallMethodHelm uses Helm chart installation
	InstallMethodHelm InstallMethod = "helm"
)

// RunOptions configures the Run test execution
type RunOptions struct {
	// HasWebhook indicates if webhooks are enabled
	HasWebhook bool
	// HasMetrics indicates if metrics are enabled
	HasMetrics bool
	// HasNetworkPolicies indicates if network policies are enabled
	HasNetworkPolicies bool
	// IsNamespaced indicates if project is namespace-scoped
	IsNamespaced bool
	// InstallMethod specifies how to install the project
	InstallMethod InstallMethod
	// HelmFullnameOverride sets fullnameOverride for Helm installations (only for InstallMethodHelm)
	HelmFullnameOverride string
	// SkipChartGeneration skips build-installer and chart generation (chart already prepared externally)
	SkipChartGeneration bool
}

// Run executes common e2e tests for a scaffolded project.
// This function is shared between go/v4 and helm/v2-alpha plugin tests.
func Run(kbc *utils.TestContext, opts RunOptions) {
	var controllerPodName string
	var err error

	// Determine the name prefix for resources
	// If fullnameOverride is set, use that; otherwise use e2e-{suffix}
	namePrefix := fmt.Sprintf("e2e-%s", kbc.TestSuffix)
	if opts.HelmFullnameOverride != "" {
		namePrefix = opts.HelmFullnameOverride
	}

	// For Helm installations with fullnameOverride, update ServiceAccount name
	if opts.InstallMethod == InstallMethodHelm {
		kbc.Kubectl.ServiceAccount = namePrefix + "-controller-manager"
	}

	By("creating manager namespace")
	err = kbc.CreateManagerNamespace()
	Expect(err).NotTo(HaveOccurred())

	By("labeling the namespace to enforce the restricted security policy")
	err = kbc.LabelNamespacesToEnforceRestricted()
	Expect(err).NotTo(HaveOccurred())

	By("updating the go.mod")
	err = kbc.Tidy()
	Expect(err).NotTo(HaveOccurred())

	By("run make all")
	err = kbc.Make("all")
	Expect(err).NotTo(HaveOccurred())

	By("building the controller image")
	err = kbc.Make("docker-build", "IMG="+kbc.ImageName)
	Expect(err).NotTo(HaveOccurred())

	By("loading the controller docker image into the kind cluster")
	err = kbc.LoadImageToKindCluster()
	Expect(err).NotTo(HaveOccurred())

	// Deploy based on installation method
	switch opts.InstallMethod {
	case InstallMethodKustomize:
		By("deploying the controller-manager via make deploy")
		cmd := exec.Command("make", "deploy", "IMG="+kbc.ImageName)
		_, err = kbc.Run(cmd)
		Expect(err).NotTo(HaveOccurred())

	case InstallMethodInstaller:
		By("building the installer")
		err = kbc.Make("build-installer", "IMG="+kbc.ImageName)
		Expect(err).NotTo(HaveOccurred())

		By("deploying the controller-manager with the installer")
		_, err = kbc.Kubectl.Apply(true, "-f", "dist/install.yaml")
		Expect(err).NotTo(HaveOccurred())

	case InstallMethodHelm:
		if !opts.SkipChartGeneration {
			By("building the installer manifest for helm chart generation")
			err = kbc.Make("build-installer", "IMG="+kbc.ImageName)
			Expect(err).NotTo(HaveOccurred(), "Failed to build installer manifest")

			By("building the helm-chart")
			err = kbc.EditHelmPlugin()
			Expect(err).NotTo(HaveOccurred(), "Failed to edit helm plugin")
		}

		By("deploying the controller-manager via Helm")
		err = kbc.HelmInstallRelease()
		Expect(err).NotTo(HaveOccurred(), "Failed to install Helm release")
	}

	By("Checking controllerManager and getting the name of the Pod")
	controllerPodName = GetControllerPodName(kbc)

	By("Checking if all flags are applied to the manager pod")
	podOutput, err := kbc.Kubectl.Get(
		true,
		"pod", controllerPodName,
		"-o", "jsonpath={.spec.containers[0].args}",
	)
	Expect(err).NotTo(HaveOccurred())
	Expect(podOutput).To(ContainSubstring("leader-elect"),
		"Expected manager pod to have --leader-elect flag")
	Expect(podOutput).To(ContainSubstring("health-probe-bind-address"),
		"Expected manager pod to have --health-probe-bind-address flag")

	By("validating that the Prometheus manager has provisioned the Service")
	Eventually(func(g Gomega) {
		_, err = kbc.Kubectl.Get(
			false,
			"Service", "prometheus-operator")
		g.Expect(err).NotTo(HaveOccurred())
	}, time.Minute, time.Second).Should(Succeed())

	By("validating that the ServiceMonitor for Prometheus is applied in the namespace")
	_, err = kbc.Kubectl.Get(
		true,
		"ServiceMonitor")
	Expect(err).NotTo(HaveOccurred())

	if opts.HasNetworkPolicies {
		if opts.HasMetrics {
			By("labeling the namespace to allow consume the metrics")
			Expect(kbc.Kubectl.Command("label", "namespaces", kbc.Kubectl.Namespace,
				"metrics=enabled")).Error().NotTo(HaveOccurred())

			By("Ensuring the Allow Metrics Traffic NetworkPolicy exists", func() {
				var output string
				output, err = kbc.Kubectl.Get(
					true,
					"networkpolicy", fmt.Sprintf("e2e-%s-allow-metrics-traffic", kbc.TestSuffix),
				)
				Expect(err).NotTo(HaveOccurred(), "NetworkPolicy allow-metrics-traffic should exist in the namespace")
				Expect(output).To(ContainSubstring("allow-metrics-traffic"), "NetworkPolicy allow-metrics-traffic "+
					"should be present in the output")
			})
		}

		if opts.HasWebhook {
			By("labeling the namespace to allow webhooks traffic")
			_, err = kbc.Kubectl.Command("label", "namespaces", kbc.Kubectl.Namespace,
				"webhook=enabled")
			Expect(err).NotTo(HaveOccurred())

			By("Ensuring the allow-webhook-traffic NetworkPolicy exists", func() {
				var output string
				output, err = kbc.Kubectl.Get(
					true,
					"networkpolicy", fmt.Sprintf("e2e-%s-allow-webhook-traffic", kbc.TestSuffix),
				)
				Expect(err).NotTo(HaveOccurred(), "NetworkPolicy allow-webhook-traffic should exist in the namespace")
				Expect(output).To(ContainSubstring("allow-webhook-traffic"), "NetworkPolicy allow-webhook-traffic "+
					"should be present in the output")
			})
		}
	}

	if opts.HasWebhook {
		By("validating that cert-manager has provisioned the certificate Secret")

		verifyWebhookCert := func(g Gomega) {
			var output string
			output, err = kbc.Kubectl.Get(
				true,
				"secrets", "webhook-server-cert")
			g.Expect(err).ToNot(HaveOccurred(), "webhook-server-cert should exist in the namespace")
			g.Expect(output).To(ContainSubstring("webhook-server-cert"))
		}

		Eventually(verifyWebhookCert, defaultTimeout, defaultPollingInterval).Should(Succeed())

		By("validating that the mutating|validating webhooks have the CA injected")
		verifyCAInjection := func(g Gomega) {
			var mwhOutput, vwhOutput string
			mwhOutput, err = kbc.Kubectl.Get(
				false,
				"mutatingwebhookconfigurations.admissionregistration.k8s.io",
				fmt.Sprintf("%s-mutating-webhook-configuration", namePrefix),
				"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
			g.Expect(err).NotTo(HaveOccurred())
			// check that ca should be long enough, because there may be a place holder "\n"
			g.Expect(len(mwhOutput)).To(BeNumerically(">", 10))

			vwhOutput, err = kbc.Kubectl.Get(
				false,
				"validatingwebhookconfigurations.admissionregistration.k8s.io",
				fmt.Sprintf("%s-validating-webhook-configuration", namePrefix),
				"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
			g.Expect(err).NotTo(HaveOccurred())
			// check that ca should be long enough, because there may be a place holder "\n"
			g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
		}

		Eventually(verifyCAInjection, defaultTimeout, defaultPollingInterval).Should(Succeed())

		By("validating that the CA injection is applied for CRD conversion")
		crdKind := "ConversionTest"
		verifyCAInjection = func(g Gomega) {
			var crdOutput string
			crdOutput, err = kbc.Kubectl.Get(
				false,
				"customresourcedefinition.apiextensions.k8s.io",
				"-o", fmt.Sprintf(
					"jsonpath={.items[?(@.spec.names.kind=='%s')].spec.conversion.webhook.clientConfig.caBundle}",
					crdKind),
			)
			g.Expect(err).NotTo(HaveOccurred(),
				"failed to get CRD conversion webhook configuration")

			// Check if the CA bundle is populated (length > 10 to avoid placeholder values)
			g.Expect(len(crdOutput)).To(BeNumerically(">", 10),
				"CA bundle should be injected into the CRD")
		}
		Eventually(verifyCAInjection, defaultTimeout, defaultPollingInterval).Should(Succeed(),
			"CA injection validation failed")

		By("waiting for the webhook service endpoints to be ready")
		verifyWebhookEndpointsReady := func(g Gomega) {
			var output string
			output, err = kbc.Kubectl.Get(
				true,
				"endpointslices.discovery.k8s.io",
				"-l", fmt.Sprintf("kubernetes.io/service-name=%s-webhook-service", namePrefix),
				"-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}")
			g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist")
			g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready")
		}
		Eventually(verifyWebhookEndpointsReady, defaultTimeout, defaultPollingInterval).Should(Succeed())

		By("waiting additional time for webhook server to stabilize")
		time.Sleep(5 * time.Second)
	}

	By("creating an instance of the CR")
	// currently controller-runtime doesn't provide a readiness probe, we retry a few times
	// we can change it to probe the readiness endpoint after CR supports it.
	sampleFile := filepath.Join("config", "samples",
		fmt.Sprintf("%s_%s_%s.yaml", kbc.Group, kbc.Version, strings.ToLower(kbc.Kind)))
	sampleFilePath, err := filepath.Abs(filepath.Join(fmt.Sprintf("e2e-%s", kbc.TestSuffix), sampleFile))
	Expect(err).To(Not(HaveOccurred()))

	// Add a field to the sample CR for testing
	err = util.ReplaceInFile(sampleFilePath, "# TODO(user): Add fields here", "foo: bar")
	Expect(err).To(Not(HaveOccurred()))

	applySample := func(g Gomega) {
		g.Expect(kbc.Kubectl.Apply(true, "-f", sampleFile)).
			Error().NotTo(HaveOccurred())
	}
	Eventually(applySample, defaultTimeout, defaultPollingInterval).Should(Succeed())

	if opts.HasMetrics {
		By("checking the metrics values to validate that the created resource object gets reconciled")
		metricsOutput := GetMetricsOutput(controllerPodName, namePrefix, kbc)
		Expect(metricsOutput).To(ContainSubstring(fmt.Sprintf(
			`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
			strings.ToLower(kbc.Kind),
		)))
	}

	if !opts.HasMetrics {
		By("validating the metrics endpoint is not working as expected")
		ValidateMetricsUnavailable(namePrefix, kbc)
	}

	if opts.HasWebhook {
		By("validating that mutating and validating webhooks are working fine")
		var cnt string
		cnt, err = kbc.Kubectl.Get(
			true,
			"-f", sampleFile,
			"-o", "go-template={{ .spec.count }}")
		Expect(err).NotTo(HaveOccurred())
		count, err2 := strconv.Atoi(cnt)
		Expect(err2).NotTo(HaveOccurred())
		Expect(count).To(BeNumerically("==", 5))
	}

	if opts.HasWebhook {
		By("creating a namespace")
		namespace := "test-webhooks"
		_, err = kbc.Kubectl.Command("create", "namespace", namespace)
		Expect(err).To(Not(HaveOccurred()), "namespace should be created successfully")

		By("applying the CR in the created namespace")

		applySampleNamespaced := func(g Gomega) {
			_, err = kbc.Kubectl.Apply(false, "-n", namespace, "-f", sampleFile)
			g.Expect(err).To(Not(HaveOccurred()))
		}
		Eventually(applySampleNamespaced, 2*time.Minute, time.Second).Should(Succeed())

		// Note: Webhooks are cluster-scoped and validate/mutate CRs in ALL namespaces,
		// even in namespace-scoped managers. The manager won't reconcile CRs outside
		// its WATCH_NAMESPACE, but webhooks will still enforce validation/mutation rules.
		By("validating that mutating webhooks are working fine outside of the manager's namespace")
		var cnt string
		cnt, err = kbc.Kubectl.Get(
			false,
			"-n", namespace,
			"-f", sampleFile,
			"-o", "go-template={{ .spec.count }}")
		Expect(err).NotTo(HaveOccurred())

		count, err2 := strconv.Atoi(cnt)
		Expect(err2).NotTo(HaveOccurred())
		Expect(count).To(BeNumerically("==", 5),
			"the mutating webhook should set the count to 5")

		By("removing the namespace")
		Expect(kbc.Kubectl.Command("delete", "namespace", namespace)).
			Error().NotTo(HaveOccurred(), "namespace should be removed successfully")

		By("validating the conversion")

		// Update the ConversionTest CR sample in v1 to set a specific `size`
		By("modifying the ConversionTest CR sample to set `size` for conversion testing")
		conversionCRFile := filepath.Join("config", "samples",
			fmt.Sprintf("%s_v1_conversiontest.yaml", kbc.Group))
		conversionCRPath := filepath.Join(kbc.Dir, conversionCRFile)

		// Edit the file to include `size` in the spec field for v1
		err = util.ReplaceInFile(conversionCRPath, "# TODO(user): Add fields here", `size: 3`)
		Expect(err).NotTo(HaveOccurred(), "failed to replace spec in ConversionTest CR sample")

		// Apply the ConversionTest Custom Resource in v1
		By("applying the modified ConversionTest CR in v1 for conversion")
		_, err = kbc.Kubectl.Apply(true, "-f", conversionCRPath)
		Expect(err).NotTo(HaveOccurred(), "failed to apply modified ConversionTest CR")

		By("waiting for the ConversionTest CR to appear")
		Eventually(func(g Gomega) {
			_, getErr := kbc.Kubectl.Get(true, "conversiontest", "conversiontest-sample")
			g.Expect(getErr).NotTo(HaveOccurred(), "expected the ConversionTest CR to exist")
		}, defaultTimeout, defaultPollingInterval).Should(Succeed())

		By("validating that the converted resource in v2 has replicas == 3")
		Eventually(func(g Gomega) {
			out, getErr := kbc.Kubectl.Get(
				true,
				"conversiontest", "conversiontest-sample",
				"-o", "jsonpath={.spec.replicas}",
			)
			g.Expect(getErr).NotTo(HaveOccurred(), "failed to get converted resource in v2")
			replicas, atoiErr := strconv.Atoi(out)
			g.Expect(atoiErr).NotTo(HaveOccurred(), "replicas field is not an integer")
			g.Expect(replicas).To(Equal(3), "expected replicas to be 3 after conversion")
		}, defaultTimeout, defaultPollingInterval).Should(Succeed())

		if opts.HasMetrics {
			By("validating conversion metrics to confirm conversion operations")
			metricsOutput := GetMetricsOutput(controllerPodName, namePrefix, kbc)
			conversionMetric := `controller_runtime_reconcile_total{controller="conversiontest",result="success"} 1`
			Expect(metricsOutput).To(ContainSubstring(conversionMetric),
				"Expected metric for successful ConversionTest reconciliation")
		}
	}

	// Validate namespace-scoped behavior: operator should NOT reconcile resources outside its namespace
	if opts.IsNamespaced {
		By("validating that namespace-scoped operator does not reconcile resources outside its namespace")

		// Create a test namespace outside the operator's watch namespace
		testNamespace := "test-out-of-scope"
		_, err = kbc.Kubectl.Command("create", "namespace", testNamespace)
		Expect(err).NotTo(HaveOccurred(), "test namespace should be created successfully")

		By("creating a CR in the out-of-scope namespace")
		// Apply the same sample CR but in the test namespace
		_, err = kbc.Kubectl.Apply(false, "-n", testNamespace, "-f", sampleFile)
		Expect(err).NotTo(HaveOccurred(), "CR should be created in test namespace")

		// Wait a bit to ensure the controller would have time to reconcile if it was watching
		time.Sleep(5 * time.Second)

		By("verifying the CR was NOT reconciled (no status conditions set)")
		// Get the CR and check if it has been reconciled by looking at its status
		crName := strings.ToLower(kbc.Kind) + "-sample"
		crOutput, err := kbc.Kubectl.Get(false, "-n", testNamespace,
			strings.ToLower(kbc.Kind), crName,
			"-o", "jsonpath={.status}")
		Expect(err).NotTo(HaveOccurred(), "CR should exist in test namespace")

		// The status should be empty or not contain conditions set by the controller
		// because the namespace-scoped operator should not be watching this namespace
		Expect(crOutput).To(Or(
			BeEmpty(),
			Not(ContainSubstring("conditions")),
		), "CR in out-of-scope namespace should not have been reconciled by the controller")

		By("cleaning up the test namespace")
		_, err = kbc.Kubectl.Command("delete", "namespace", testNamespace, "--timeout=60s")
		Expect(err).NotTo(HaveOccurred(), "test namespace should be deleted successfully")
	}
}


================================================
FILE: test/e2e/internal/helpers/plugin_test_metrics.go
================================================
/*
Copyright 2024 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 helpers

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	. "github.com/onsi/ginkgo/v2" //nolint:staticcheck
	. "github.com/onsi/gomega"    //nolint:staticcheck

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
	"sigs.k8s.io/kubebuilder/v4/test/e2e/utils"
)

const (
	tokenRequestRawString = `{"apiVersion": "authentication.k8s.io/v1", "kind": "TokenRequest"}`
)

// tokenRequest is a trimmed down version of the authentication.k8s.io/v1/TokenRequest Type
// that we want to use for extracting the token.
type tokenRequest struct {
	Status struct {
		Token string `json:"token"`
	} `json:"status"`
}

// GetControllerPodName validates that the controller-manager pod is running and returns its name
func GetControllerPodName(kbc *utils.TestContext) string {
	By("validating that the controller-manager pod is running as expected")
	var controllerPodName string
	verifyControllerUp := func(g Gomega) error {
		// Get pod name
		podOutput, err := kbc.Kubectl.Get(
			true,
			"pods", "-l", "control-plane=controller-manager",
			"-o", "go-template={{ range .items }}{{ if not .metadata.deletionTimestamp }}{{ .metadata.name }}"+
				"{{ \"\\n\" }}{{ end }}{{ end }}")
		g.Expect(err).NotTo(HaveOccurred())
		podNames := util.GetNonEmptyLines(podOutput)
		if len(podNames) != 1 {
			return fmt.Errorf("expect 1 controller pods running, but got %d", len(podNames))
		}
		controllerPodName = podNames[0]
		g.Expect(controllerPodName).Should(ContainSubstring("controller-manager"))

		// Validate pod status
		status, err := kbc.Kubectl.Get(
			true,
			"pods", controllerPodName, "-o", "jsonpath={.status.phase}")
		g.Expect(err).NotTo(HaveOccurred())
		if status != "Running" {
			return fmt.Errorf("controller pod in %s status", status)
		}
		return nil
	}
	defer func() {
		out, err := kbc.Kubectl.CommandInNamespace("describe", "all")
		Expect(err).NotTo(HaveOccurred())
		_, _ = fmt.Fprintln(GinkgoWriter, out)
	}()
	Eventually(verifyControllerUp, 5*time.Minute, time.Second).Should(Succeed())
	return controllerPodName
}

// GetMetricsOutput returns the metrics output from curl pod
// namePrefix is the prefix for service names (e.g., "e2e-{suffix}" or "custom-operator" from fullnameOverride)
func GetMetricsOutput(controllerPodName, namePrefix string, kbc *utils.TestContext) string {
	var err error
	// All Kubebuilder projects are cluster-scoped, so use ClusterRoleBinding
	_, err = kbc.Kubectl.Command(
		"get", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
	)
	if err != nil && strings.Contains(err.Error(), "NotFound") {
		_, err = kbc.Kubectl.Command(
			"create", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
			fmt.Sprintf("--clusterrole=%s-metrics-reader", namePrefix),
			fmt.Sprintf("--serviceaccount=%s:%s", kbc.Kubectl.Namespace, kbc.Kubectl.ServiceAccount),
		)
		Expect(err).NotTo(HaveOccurred())
	} else {
		Expect(err).NotTo(HaveOccurred(), "Failed to check clusterrolebinding existence")
	}

	token, err := serviceAccountToken(kbc)
	Expect(err).NotTo(HaveOccurred())
	Expect(token).NotTo(BeEmpty())

	var metricsOutput string
	By("validating that the controller-manager service is available")
	_, err = kbc.Kubectl.Get(
		true,
		"service", fmt.Sprintf("%s-controller-manager-metrics-service", namePrefix),
	)
	Expect(err).NotTo(HaveOccurred(), "Controller-manager service should exist")

	By("ensuring the service endpoint is ready")
	metricsServiceName := fmt.Sprintf("%s-controller-manager-metrics-service", namePrefix)
	checkServiceEndpoint := func(g Gomega) {
		var output string
		output, err = kbc.Kubectl.Command(
			"get", "endpointslices.discovery.k8s.io",
			"-n", kbc.Kubectl.Namespace,
			"-l", fmt.Sprintf("kubernetes.io/service-name=%s", metricsServiceName),
			"-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}",
		)
		g.Expect(err).NotTo(HaveOccurred(), "endpointslices should exist")
		g.Expect(output).ShouldNot(BeEmpty(), "no endpoints found")
	}
	Eventually(checkServiceEndpoint, 2*time.Minute, time.Second).Should(Succeed(),
		"Service endpoint should be ready")

	// NOTE: On Kubernetes 1.33+, we've observed a delay before the metrics endpoint becomes available
	// when using controller-runtime's WithAuthenticationAndAuthorization() with self-signed certificates.
	// This delay appears to stem from Kubernetes itself, potentially due to changes in how it initializes
	// service account tokens or handles TLS/service readiness.
	By("ensuring the controller pod is fully ready before creating test pods")
	verifyControllerPodReady := func(g Gomega) {
		var output string
		output, err = kbc.Kubectl.Get(
			true,
			"pod", controllerPodName,
			"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}",
		)
		g.Expect(err).NotTo(HaveOccurred())
		g.Expect(output).To(Equal("True"), "Controller pod not ready")
	}
	Eventually(verifyControllerPodReady, defaultTimeout, defaultPollingInterval).Should(Succeed())

	webhookServiceName := fmt.Sprintf("%s-webhook-service", namePrefix)
	if _, err = kbc.Kubectl.Get(false, "service", webhookServiceName); err == nil {
		By("waiting for the webhook service endpoints to be ready")
		checkWebhookEndpoint := func(g Gomega) {
			var output string
			output, err = kbc.Kubectl.Command(
				"get", "endpointslices.discovery.k8s.io",
				"-n", kbc.Kubectl.Namespace,
				"-l", fmt.Sprintf("kubernetes.io/service-name=%s", webhookServiceName),
				"-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}",
			)
			g.Expect(err).NotTo(HaveOccurred(), "webhook endpoints should exist")
			g.Expect(output).ShouldNot(BeEmpty(), "webhook endpoints not yet ready")
		}
		Eventually(checkWebhookEndpoint, defaultTimeout, defaultPollingInterval).Should(Succeed(),
			"Webhook service endpoints should be ready")
	}

	By("creating a curl pod to access the metrics endpoint")
	cmdOpts := cmdOptsToCreateCurlPod(namePrefix, kbc, token)
	_, err = kbc.Kubectl.CommandInNamespace(cmdOpts...)
	Expect(err).NotTo(HaveOccurred())

	By("validating that the curl pod is running as expected")
	verifyCurlUp := func(g Gomega) {
		var status string
		status, err = kbc.Kubectl.Get(
			true,
			"pods", "curl", "-o", "jsonpath={.status.phase}")
		g.Expect(err).NotTo(HaveOccurred())
		g.Expect(status).To(Equal("Succeeded"), fmt.Sprintf("curl pod in %s status", status))
	}
	Eventually(verifyCurlUp, 240*time.Second, time.Second).Should(Succeed())

	By("validating that the correct ServiceAccount is being used")
	saName := kbc.Kubectl.ServiceAccount
	currentSAOutput, err := kbc.Kubectl.Get(
		true,
		"serviceaccount", saName,
		"-o", "jsonpath={.metadata.name}",
	)
	Expect(err).NotTo(HaveOccurred(), "Failed to fetch the service account")
	Expect(currentSAOutput).To(Equal(saName), "The ServiceAccount in use does not match the expected one")

	By("validating that the metrics endpoint is serving as expected")
	getCurlLogs := func(g Gomega) {
		metricsOutput, err = kbc.Kubectl.Logs("curl")
		g.Expect(err).NotTo(HaveOccurred())
		g.Expect(metricsOutput).Should(ContainSubstring("< HTTP/1.1 200 OK"))
	}
	Eventually(getCurlLogs, 10*time.Second, time.Second).Should(Succeed())
	removeCurlPod(kbc)
	return metricsOutput
}

// ValidateMetricsUnavailable validates that metrics are not exposed
// namePrefix is the prefix for service names (e.g., "e2e-{suffix}" or "custom-operator" from fullnameOverride)
func ValidateMetricsUnavailable(namePrefix string, kbc *utils.TestContext) {
	// All Kubebuilder projects are cluster-scoped, so use ClusterRoleBinding
	_, err := kbc.Kubectl.Command(
		"create", "clusterrolebinding", fmt.Sprintf("metrics-%s", kbc.TestSuffix),
		fmt.Sprintf("--clusterrole=%s-metrics-reader", namePrefix),
		fmt.Sprintf("--serviceaccount=%s:%s", kbc.Kubectl.Namespace, kbc.Kubectl.ServiceAccount))
	Expect(err).NotTo(HaveOccurred())

	token, err := serviceAccountToken(kbc)
	Expect(err).NotTo(HaveOccurred())
	Expect(token).NotTo(BeEmpty())

	By("creating a curl pod to access the metrics endpoint")
	cmdOpts := cmdOptsToCreateCurlPod(namePrefix, kbc, token)
	_, err = kbc.Kubectl.CommandInNamespace(cmdOpts...)
	Expect(err).NotTo(HaveOccurred())

	By("validating that the curl pod fail as expected")
	verifyCurlUp := func(g Gomega) {
		status, errCurl := kbc.Kubectl.Get(
			true,
			"pods", "curl", "-o", "jsonpath={.status.phase}")
		g.Expect(errCurl).NotTo(HaveOccurred())
		g.Expect(status).NotTo(Equal("Failed"),
			fmt.Sprintf("curl pod in %s status when should fail with an error", status))
	}
	Eventually(verifyCurlUp, 240*time.Second, time.Second).Should(Succeed())

	By("validating that the metrics endpoint is not working as expected")
	getCurlLogs := func(g Gomega) {
		metricsOutput, err := kbc.Kubectl.Logs("curl")
		g.Expect(err).NotTo(HaveOccurred())
		g.Expect(metricsOutput).Should(ContainSubstring("Could not resolve host"))
	}
	Eventually(getCurlLogs, 10*time.Second, time.Second).Should(Succeed())
	removeCurlPod(kbc)
}

func cmdOptsToCreateCurlPod(namePrefix string, kbc *utils.TestContext, token string) []string {
	cmdOpts := []string{
		"run", "curl",
		"--restart=Never",
		"--namespace", kbc.Kubectl.Namespace,
		"--image=curlimages/curl:latest",
		"--overrides",
		fmt.Sprintf(`{
			"spec": {
				"containers": [{
					"name": "curl",
					"image": "curlimages/curl:latest",
					"command": ["/bin/sh", "-c"],
					"args": [
						"for i in $(seq 1 30); do `+
			`curl -v -k -H 'Authorization: Bearer %s' `+
			`https://%s-controller-manager-metrics-service.%s.svc.cluster.local:8443/metrics `+
			`&& exit 0 || sleep 2; done; exit 1"
					],
					"securityContext": {
						"readOnlyRootFilesystem": true,
						"allowPrivilegeEscalation": false,
						"capabilities": {
							"drop": ["ALL"]
						},
						"runAsNonRoot": true,
						"runAsUser": 1000,
						"seccompProfile": {
							"type": "RuntimeDefault"
						}
					}
				}],
				"serviceAccountName": "%s"
			}
    }`, token, namePrefix, kbc.Kubectl.Namespace, kbc.Kubectl.ServiceAccount),
	}
	return cmdOpts
}

func removeCurlPod(kbc *utils.TestContext) {
	By("cleaning up the curl pod")
	_, err := kbc.Kubectl.Delete(true, "pods/curl", "--grace-period=0", "--force")
	Expect(err).NotTo(HaveOccurred())
}

// serviceAccountToken provides a helper function that can provide you with a service account
// token that you can use to interact with the service. This function leverages the k8s'
// TokenRequest API in raw format in order to make it generic for all version of the k8s that
// is currently being supported in kubebuilder test infra.
// TokenRequest API returns the token in raw JWT format itself. There is no conversion required.
func serviceAccountToken(kbc *utils.TestContext) (string, error) {
	var out string

	secretName := fmt.Sprintf("%s-token-request", kbc.Kubectl.ServiceAccount)
	tokenRequestFile := filepath.Join(kbc.Dir, secretName)
	if err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o755)); err != nil {
		return out, fmt.Errorf("error creating token request file %s: %w", tokenRequestFile, err)
	}
	getToken := func(g Gomega) {
		// Output of this is already a valid JWT token. No need to covert this from base64 to string format
		rawJSON, err := kbc.Kubectl.Command(
			"create",
			"--raw", fmt.Sprintf(
				"/api/v1/namespaces/%s/serviceaccounts/%s/token",
				kbc.Kubectl.Namespace,
				kbc.Kubectl.ServiceAccount,
			),
			"-f", tokenRequestFile,
		)

		g.Expect(err).NotTo(HaveOccurred())
		var token tokenRequest
		err = json.Unmarshal([]byte(rawJSON), &token)
		g.Expect(err).NotTo(HaveOccurred())

		out = token.Status.Token
	}
	Eventually(getToken, 2*time.Minute, time.Second).Should(Succeed())

	return out, nil
}


================================================
FILE: test/e2e/kind-config.yaml
================================================
#  Copyright 2020 The Kubernetes Authors.
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  disableDefaultCNI: false # Let it use default CNI so we can test NetworkPolicies
nodes:
  - role: control-plane


================================================
FILE: test/e2e/local.sh
================================================
#!/usr/bin/env bash

# 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.

source "$(dirname "$0")/../common.sh"
source "$(dirname "$0")/setup.sh"

export KIND_CLUSTER="local-kubebuilder-e2e"
create_cluster ${KIND_K8S_VERSION}
if [ -z "${SKIP_KIND_CLEANUP:-}" ]; then
  trap delete_cluster EXIT
fi

# Wait a moment for the cluster control plane to be fully initialized
echo "Ensuring cluster is fully initialized..."
sleep 5

# Export kubeconfig for the cluster
kind export kubeconfig --kubeconfig $tmp_root/kubeconfig --name $KIND_CLUSTER
export KUBECONFIG=$tmp_root/kubeconfig

# Verify we can communicate with the cluster
kubectl cluster-info
echo "Cluster is ready for testing"

test_cluster -v -ginkgo.v


================================================
FILE: test/e2e/setup.sh
================================================
#!/usr/bin/env bash

# Copyright 2019 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.

build_kb
fetch_tools
install_kind

# Creates a named kind cluster given a k8s version.
# The KIND_CLUSTER variable defines the cluster name and
# is expected to be defined in the calling environment.
#
# Usage:
#
#   export KIND_CLUSTER=
#   create_cluster 
function create_cluster {
  echo "Getting kind config..."
  KIND_VERSION=$1
  : ${KIND_CLUSTER:?"KIND_CLUSTER must be set"}
  : ${1:?"k8s version must be set as arg 1"}
  if ! kind get clusters | grep -q $KIND_CLUSTER ; then
    version_prefix="${KIND_VERSION%.*}"
    kind_config=$(dirname "$0")/kind-config.yaml
    if test -f $(dirname "$0")/kind-config-${version_prefix}.yaml; then
      kind_config=$(dirname "$0")/kind-config-${version_prefix}.yaml
    fi
    echo "Creating cluster..."
    kind create cluster -v 4 --name $KIND_CLUSTER --retain --wait=5m --config ${kind_config} --image=kindest/node:$1
    
    echo "Waiting for cluster to be fully ready..."
    kubectl wait --for=condition=Ready nodes --all --timeout=5m
  fi
}

# Deletes a kind cluster by cluster name.
# The KIND_CLUSTER variable defines the cluster name and
# is expected to be defined in the calling environment.
#
# Usage:
#
#   export KIND_CLUSTER=
#   delete_cluster
function delete_cluster {
  : ${KIND_CLUSTER:?"KIND_CLUSTER must be set"}
  kind delete cluster --name $KIND_CLUSTER
}

function test_cluster {
  local flags="$@"

  # Detect the platform architecture for the kind cluster
  # Kind clusters now run natively on the host architecture (arm64 on Apple Silicon, amd64 on x86)
  local kind_platform="linux/amd64"
  if [[ "$OSTYPE" == "darwin"* ]] && [[ "$(uname -m)" == "arm64" ]]; then
    kind_platform="linux/arm64"
  elif [[ "$(uname -m)" == "aarch64" ]]; then
    kind_platform="linux/arm64"
  fi

  # Pull images for the correct platform
  docker pull --platform ${kind_platform} memcached:1.6.26-alpine3.19
  docker pull --platform ${kind_platform} busybox:1.36.1
  docker pull --platform ${kind_platform} bitnami/kubectl:latest

  # Load images directly with ctr to avoid kind's --all-platforms issue
  # kind load docker-image uses --all-platforms internally which breaks with multi-platform manifests
  docker save memcached:1.6.26-alpine3.19 | docker exec -i $KIND_CLUSTER-control-plane ctr --namespace=k8s.io images import /dev/stdin
  
  # Busybox has Docker save issues on some platforms, pull directly as fallback
  if ! docker save busybox:1.36.1 2>/dev/null | docker exec -i $KIND_CLUSTER-control-plane ctr --namespace=k8s.io images import /dev/stdin 2>/dev/null; then
    docker exec $KIND_CLUSTER-control-plane ctr --namespace=k8s.io images pull --platform ${kind_platform} docker.io/library/busybox:1.36.1 >/dev/null 2>&1
  fi

  go test $(dirname "$0")/all $flags -timeout 60m

  docker save bitnami/kubectl:latest | docker exec -i $KIND_CLUSTER-control-plane ctr --namespace=k8s.io images import /dev/stdin
}


================================================
FILE: test/e2e/utils/kubectl.go
================================================
/*
Copyright 2019 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 utils

import (
	"encoding/json"
	"errors"
	"fmt"
	"os/exec"
	"strconv"
	"strings"
)

// Kubectl contains context to run kubectl commands
type Kubectl struct {
	*CmdContext
	Namespace      string
	ServiceAccount string
}

// Command is a general func to run kubectl commands
func (k *Kubectl) Command(cmdOptions ...string) (string, error) {
	cmd := exec.Command("kubectl", cmdOptions...)
	output, err := k.Run(cmd)
	return string(output), err
}

// WithInput is a general func to run kubectl commands with input
func (k *Kubectl) WithInput(stdinInput string) *Kubectl {
	k.Stdin = strings.NewReader(stdinInput)
	return k
}

// CommandInNamespace is a general func to run kubectl commands in the namespace
func (k *Kubectl) CommandInNamespace(cmdOptions ...string) (string, error) {
	if len(k.Namespace) == 0 {
		return "", errors.New("namespace should not be empty")
	}
	return k.Command(append([]string{"-n", k.Namespace}, cmdOptions...)...)
}

// Apply is a general func to run kubectl apply commands
func (k *Kubectl) Apply(inNamespace bool, cmdOptions ...string) (string, error) {
	ops := append([]string{"apply"}, cmdOptions...)
	if inNamespace {
		return k.CommandInNamespace(ops...)
	}
	return k.Command(ops...)
}

// Get is a func to run kubectl get commands
func (k *Kubectl) Get(inNamespace bool, cmdOptions ...string) (string, error) {
	ops := append([]string{"get"}, cmdOptions...)
	if inNamespace {
		return k.CommandInNamespace(ops...)
	}
	return k.Command(ops...)
}

// Delete is a func to run kubectl delete commands
func (k *Kubectl) Delete(inNamespace bool, cmdOptions ...string) (string, error) {
	ops := append([]string{"delete"}, cmdOptions...)
	if inNamespace {
		return k.CommandInNamespace(ops...)
	}
	return k.Command(ops...)
}

// Logs is a func to run kubectl logs commands
func (k *Kubectl) Logs(cmdOptions ...string) (string, error) {
	ops := append([]string{"logs"}, cmdOptions...)
	return k.CommandInNamespace(ops...)
}

// Wait is a func to run kubectl wait commands
func (k *Kubectl) Wait(inNamespace bool, cmdOptions ...string) (string, error) {
	ops := append([]string{"wait"}, cmdOptions...)
	if inNamespace {
		return k.CommandInNamespace(ops...)
	}
	return k.Command(ops...)
}

// VersionInfo holds a subset of client/server version information.
type VersionInfo struct {
	Major      string `json:"major"`
	Minor      string `json:"minor"`
	GitVersion string `json:"gitVersion"`

	// Leaving major/minor int fields unexported prevents them from being set
	// while leaving their exported counterparts untouched -> incorrect marshaled format.
	major, minor uint64
}

// GetMajorInt returns the uint64 representation of vi.Major.
func (vi VersionInfo) GetMajorInt() uint64 { return vi.major }

// GetMinorInt returns the uint64 representation of vi.Minor.
func (vi VersionInfo) GetMinorInt() uint64 { return vi.minor }

func (vi *VersionInfo) parseVersionInts() (err error) {
	if vi.Major != "" {
		if vi.major, err = strconv.ParseUint(vi.Major, 10, 64); err != nil {
			return fmt.Errorf("error parsing major version %q: %w", vi.Major, err)
		}
	}
	if vi.Minor != "" {
		if vi.minor, err = strconv.ParseUint(vi.Minor, 10, 64); err != nil {
			return fmt.Errorf("error parsing minor version %q: %w", vi.Minor, err)
		}
	}
	return nil
}

// KubernetesVersion holds a subset of both client and server versions.
type KubernetesVersion struct {
	ClientVersion VersionInfo `json:"clientVersion,omitempty"`
	ServerVersion VersionInfo `json:"serverVersion,omitempty"`
}

func (v *KubernetesVersion) prepare() error {
	if err := v.ClientVersion.parseVersionInts(); err != nil {
		return err
	}
	return v.ServerVersion.parseVersionInts()
}

// Version is a func to run kubectl version command
func (k *Kubectl) Version() (ver KubernetesVersion, err error) {
	out, err := k.Command("version", "-o", "json")
	if err != nil {
		return KubernetesVersion{}, fmt.Errorf("error getting kubernetes version: %w", err)
	}
	if decodeErr := ver.decode(out); decodeErr != nil {
		return KubernetesVersion{}, fmt.Errorf("error parsing kubernetes version: %w", decodeErr)
	}
	return ver, nil
}

func (v *KubernetesVersion) decode(out string) error {
	dec := json.NewDecoder(strings.NewReader(out))
	if err := dec.Decode(&v); err != nil {
		return fmt.Errorf("error decoding kubernetes version: %w", err)
	}
	return v.prepare()
}


================================================
FILE: test/e2e/utils/kubectl_test.go
================================================
/*
Copyright 2022 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 utils

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

var _ = Describe("Kubectl", func() {
	var ver KubernetesVersion
	AfterEach(func() {
		ver = KubernetesVersion{}
	})
	Context("successful 'kubectl version' output", func() {
		It("decodes both client and server versions", func() {
			Expect(ver.decode(clientServerOutput)).To(Succeed())
			Expect(ver.ClientVersion.major).To(BeNumerically("==", 1))
			Expect(ver.ClientVersion.minor).To(BeNumerically("==", 21))
			Expect(ver.ServerVersion.major).To(BeNumerically("==", 1))
			Expect(ver.ServerVersion.minor).To(BeNumerically("==", 21))
		})
		It("decodes only client version", func() {
			Expect(ver.decode(clientOnlyOutput)).To(Succeed())
			Expect(ver.ClientVersion.major).To(BeNumerically("==", 1))
			Expect(ver.ClientVersion.minor).To(BeNumerically("==", 21))
			Expect(ver.ServerVersion.major).To(BeNumerically("==", 0))
			Expect(ver.ServerVersion.minor).To(BeNumerically("==", 0))
		})
	})
	Context("'kubectl version' output with non-JSON text", func() {
		It("handles warning logs", func() {
			Expect(ver.decode(clientServerWithWarningOutput)).To(Succeed())
			Expect(ver.ClientVersion.major).To(BeNumerically("==", 1))
			Expect(ver.ClientVersion.minor).To(BeNumerically("==", 21))
			Expect(ver.ServerVersion.major).To(BeNumerically("==", 1))
			Expect(ver.ServerVersion.minor).To(BeNumerically("==", 21))
		})
	})
	Context("with error text", func() {
		It("returns an error", func() {
			Expect(ver.decode(errorOutput)).NotTo(Succeed())
		})
	})
})

const clientServerOutput = `
{
  "clientVersion": {
    "major": "1",
    "minor": "21",
    "gitVersion": "v0.21.0-beta.1",
    "gitCommit": "0d10c3f72592addce965b9bb34992eb6fc283a3b",
    "gitTreeState": "clean",
    "buildDate": "2021-08-31T22:03:33Z",
    "goVersion": "go1.16.6",
    "compiler": "gc",
    "platform": "linux/amd64"
  },
  "serverVersion": {
    "major": "1",
    "minor": "21",
    "gitVersion": "v1.21.1",
    "gitCommit": "5e58841cce77d4bc13713ad2b91fa0d961e69192",
    "gitTreeState": "clean",
    "buildDate": "2021-05-18T01:10:20Z",
    "goVersion": "go1.16.4",
    "compiler": "gc",
    "platform": "linux/amd64"
  }
}
`

const clientOnlyOutput = `
{
  "clientVersion": {
    "major": "1",
    "minor": "21",
    "gitVersion": "v0.21.0-beta.1",
    "gitCommit": "0d10c3f72592addce965b9bb34992eb6fc283a3b",
    "gitTreeState": "clean",
    "buildDate": "2021-08-31T22:03:33Z",
    "goVersion": "go1.16.6",
    "compiler": "gc",
    "platform": "linux/amd64"
  }
}
`

const clientServerWithWarningOutput = `
{
  "clientVersion": {
    "major": "1",
    "minor": "21",
    "gitVersion": "v0.21.0-beta.1",
    "gitCommit": "0d10c3f72592addce965b9bb34992eb6fc283a3b",
    "gitTreeState": "clean",
    "buildDate": "2021-08-31T22:03:33Z",
    "goVersion": "go1.16.6",
    "compiler": "gc",
    "platform": "linux/amd64"
  },
  "serverVersion": {
    "major": "1",
    "minor": "21",
    "gitVersion": "v1.21.1",
    "gitCommit": "5e58841cce77d4bc13713ad2b91fa0d961e69192",
    "gitTreeState": "clean",
    "buildDate": "2021-05-18T01:10:20Z",
    "goVersion": "go1.16.4",
    "compiler": "gc",
    "platform": "linux/amd64"
  }
}
WARNING: version difference between client (0.21) and server (1.21) exceeds the supported minor version skew of +/-1
`

const errorOutput = `
ERROR: reason blah blah
`


================================================
FILE: test/e2e/utils/suite_test.go
================================================
/*
Copyright 2022 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 utils

import (
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func TestUtils(t *testing.T) {
	RegisterFailHandler(Fail)
	RunSpecs(t, "Utils Suite")
}


================================================
FILE: test/e2e/utils/test_context.go
================================================
/*
Copyright 2019 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 utils

import (
	"fmt"
	"io"
	log "log/slog"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	//nolint:staticcheck
	. "github.com/onsi/ginkgo/v2"

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
)

const (
	certmanagerVersion = "v1.20.0"
	certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"

	defaultKindCluster = "kind"
	defaultKindBinary  = "kind"

	prometheusOperatorVersion = "v0.89.0"
	prometheusOperatorURL     = "https://github.com/prometheus-operator/prometheus-operator/" +
		"releases/download/%s/bundle.yaml"
)

// TestContext specified to run e2e tests
type TestContext struct {
	*CmdContext
	TestSuffix   string
	Domain       string
	Group        string
	Version      string
	Kind         string
	Resources    string
	ImageName    string
	BinaryName   string
	Kubectl      *Kubectl
	K8sVersion   *KubernetesVersion
	IsRestricted bool
}

// NewTestContext init with a random suffix for test TestContext stuff,
// to avoid conflict when running tests synchronously.
func NewTestContext(binaryName string, env ...string) (*TestContext, error) {
	testSuffix, err := util.RandomSuffix()
	if err != nil {
		return nil, fmt.Errorf("failed to generate random suffix: %w", err)
	}

	cc := &CmdContext{
		Env: env,
	}

	// Use kubectl to get Kubernetes client and cluster version.
	kubectl := &Kubectl{
		Namespace:      fmt.Sprintf("e2e-%s-system", testSuffix),
		ServiceAccount: fmt.Sprintf("e2e-%s-controller-manager", testSuffix),
		CmdContext:     cc,
	}

	// For test outside of cluster we do not need to have kubectl
	var k8sVersion *KubernetesVersion
	fakeVersion := &KubernetesVersion{
		ClientVersion: VersionInfo{
			Major:      "1",
			Minor:      "0",
			GitVersion: "v1.0.0-fake",
		},
		ServerVersion: VersionInfo{
			Major:      "1",
			Minor:      "0",
			GitVersion: "v1.0.0-fake",
		},
	}

	var v KubernetesVersion
	var lookupErr error

	_, lookupErr = exec.LookPath("kubectl")
	if lookupErr != nil {
		_, _ = fmt.Fprintf(GinkgoWriter, "warning: kubectl not found in PATH; proceeding with fake version\n")
		k8sVersion = fakeVersion
	} else if v, err = kubectl.Version(); err != nil {
		_, _ = fmt.Fprintf(GinkgoWriter, "warning: failed to get kubernetes version: %v\n", err)
		k8sVersion = fakeVersion
	} else {
		k8sVersion = &v
	}
	// Set CmdContext.Dir after running Kubectl.Version() because dir does not exist yet.
	if cc.Dir, err = filepath.Abs("e2e-" + testSuffix); err != nil {
		return nil, fmt.Errorf("failed to determine absolute path to %q: %w", "e2e-"+testSuffix, err)
	}

	return &TestContext{
		TestSuffix: testSuffix,
		Domain:     "example.com" + testSuffix,
		Group:      "bar" + testSuffix,
		Version:    "v1alpha1",
		Kind:       "Foo" + testSuffix,
		Resources:  "foo" + testSuffix + "s",
		ImageName:  "e2e-test/controller-manager:" + testSuffix,
		CmdContext: cc,
		Kubectl:    kubectl,
		K8sVersion: k8sVersion,
		BinaryName: binaryName,
	}, nil
}

func warnError(err error) {
	_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
}

// Prepare prepares the test environment.
func (t *TestContext) Prepare() error {
	// Remove tools used by projects in the environment so the correct version is downloaded for each test.
	_, _ = fmt.Fprintln(GinkgoWriter, "cleaning up tools")
	for _, toolName := range []string{"controller-gen", "kustomize"} {
		if toolPath, err := exec.LookPath(toolName); err == nil {
			if err := os.RemoveAll(toolPath); err != nil {
				return fmt.Errorf("failed to remove %q: %w", toolName, err)
			}
		}
	}

	_, _ = fmt.Fprintf(GinkgoWriter, "preparing testing directory: %s\n", t.Dir)
	if err := os.MkdirAll(t.Dir, 0o755); err != nil {
		return fmt.Errorf("error creating test directory %q: %w", t.Dir, err)
	}

	return nil
}

// makeCertManagerURL returns a kubectl-able URL for the cert-manager bundle.
func (t *TestContext) makeCertManagerURL() string {
	return fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
}

func (t *TestContext) makePrometheusOperatorURL() string {
	return fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
}

// InstallCertManager installs the cert manager bundle.
func (t *TestContext) InstallCertManager() error {
	url := t.makeCertManagerURL()
	if _, err := t.Kubectl.Apply(false, "-f", url, "--validate=false"); err != nil {
		return err
	}
	// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
	// was re-installed after uninstalling on a cluster.
	if _, err := t.Kubectl.Wait(false, "deployment.apps/cert-manager-webhook",
		"--for", "condition=Available",
		"--namespace", "cert-manager",
		"--timeout", "5m",
	); err != nil {
		return err
	}

	// Additional wait for webhook TLS to be fully initialized
	// The webhook needs time after deployment is ready to generate and serve valid certificates
	time.Sleep(10 * time.Second)
	return nil
}

// UninstallCertManager uninstalls the cert manager bundle.
func (t *TestContext) UninstallCertManager() {
	url := t.makeCertManagerURL()
	if _, err := t.Kubectl.Delete(false, "-f", url); err != nil {
		warnError(err)
	}
}

// InstallPrometheusOperManager installs the prometheus manager bundle.
func (t *TestContext) InstallPrometheusOperManager() error {
	url := t.makePrometheusOperatorURL()
	// Use server-side apply to handle large CRD annotations
	_, err := t.Kubectl.Command("apply", "--server-side", "-f", url)
	return err
}

// UninstallPrometheusOperManager uninstalls the prometheus manager bundle.
func (t *TestContext) UninstallPrometheusOperManager() {
	url := t.makePrometheusOperatorURL()
	if _, err := t.Kubectl.Delete(false, "-f", url); err != nil {
		warnError(err)
	}
}

// Init is for running `kubebuilder init`
func (t *TestContext) Init(initOptions ...string) error {
	initOptions = append([]string{"init"}, initOptions...)
	//nolint:gosec
	cmd := exec.Command(t.BinaryName, initOptions...)
	_, err := t.Run(cmd)
	return err
}

// Edit is for running `kubebuilder edit`
func (t *TestContext) Edit(editOptions ...string) error {
	editOptions = append([]string{"edit"}, editOptions...)
	//nolint:gosec
	cmd := exec.Command(t.BinaryName, editOptions...)
	_, err := t.Run(cmd)
	return err
}

// CreateAPI is for running `kubebuilder create api`
func (t *TestContext) CreateAPI(resourceOptions ...string) error {
	resourceOptions = append([]string{"create", "api"}, resourceOptions...)
	//nolint:gosec
	cmd := exec.Command(t.BinaryName, resourceOptions...)
	_, err := t.Run(cmd)
	return err
}

// CreateWebhook is for running `kubebuilder create webhook`
func (t *TestContext) CreateWebhook(resourceOptions ...string) error {
	resourceOptions = append([]string{"create", "webhook"}, resourceOptions...)
	//nolint:gosec
	cmd := exec.Command(t.BinaryName, resourceOptions...)
	_, err := t.Run(cmd)
	return err
}

// Regenerate is for running `kubebuilder alpha generate`
func (t *TestContext) Regenerate(resourceOptions ...string) error {
	resourceOptions = append([]string{"alpha", "generate"}, resourceOptions...)
	//nolint:gosec
	cmd := exec.Command(t.BinaryName, resourceOptions...)
	_, err := t.Run(cmd)
	return err
}

// Make is for running `make` with various targets
func (t *TestContext) Make(makeOptions ...string) error {
	cmd := exec.Command("make", makeOptions...)
	_, err := t.Run(cmd)
	return err
}

// Tidy runs `go mod tidy` so that go 1.16 build doesn't fail.
// See https://blog.golang.org/go116-module-changes#TOC_3.
func (t *TestContext) Tidy() error {
	cmd := exec.Command("go", "mod", "tidy")
	_, err := t.Run(cmd)
	return err
}

// Destroy is for cleaning up the docker images for testing
func (t *TestContext) Destroy() {
	//nolint:gosec
	// if image name is not present or not provided skip execution of docker command
	if t.ImageName != "" {
		// Check white space from image name
		if len(strings.TrimSpace(t.ImageName)) == 0 {
			log.Info("Image not set, skip cleaning up of docker image")
		} else {
			cmd := exec.Command("docker", "rmi", "-f", t.ImageName)
			if _, err := t.Run(cmd); err != nil {
				warnError(err)
			}
		}
	}
	if err := os.RemoveAll(t.Dir); err != nil {
		warnError(err)
	}
}

// CreateManagerNamespace will create the namespace where the manager is deployed
func (t *TestContext) CreateManagerNamespace() error {
	_, err := t.Kubectl.Command("create", "ns", t.Kubectl.Namespace)
	return err
}

// LabelNamespacesToEnforceRestricted will label specified namespaces so that we can verify
// if the manifests can be applied in restricted environments with strict security policy enforced
func (t *TestContext) LabelNamespacesToEnforceRestricted() error {
	_, err := t.Kubectl.Command("label", "--overwrite", "ns", t.Kubectl.Namespace,
		"pod-security.kubernetes.io/enforce=restricted")
	return err
}

// RemoveNamespaceLabelToEnforceRestricted will remove the `pod-security.kubernetes.io/enforce` label
// from the specified namespace
func (t *TestContext) RemoveNamespaceLabelToEnforceRestricted() error {
	_, err := t.Kubectl.Command("label", "ns", t.Kubectl.Namespace, "pod-security.kubernetes.io/enforce-")
	return err
}

// LoadImageToKindCluster loads a local docker image to the kind cluster
func (t *TestContext) LoadImageToKindCluster() error {
	cluster := defaultKindCluster
	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
		cluster = v
	}
	kindOptions := []string{"load", "docker-image", t.ImageName, "--name", cluster}
	kindBinary := defaultKindBinary
	if v, ok := os.LookupEnv("KIND"); ok {
		kindBinary = v
	}
	cmd := exec.Command(kindBinary, kindOptions...)
	_, err := t.Run(cmd)
	return err
}

// LoadImageToKindClusterWithName loads a local docker image with the name informed to the kind cluster
func (t TestContext) LoadImageToKindClusterWithName(image string) error {
	cluster := defaultKindCluster
	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
		cluster = v
	}
	kindOptions := []string{"load", "docker-image", "--name", cluster, image}
	kindBinary := defaultKindCluster
	if v, ok := os.LookupEnv("KIND"); ok {
		kindBinary = v
	}
	cmd := exec.Command(kindBinary, kindOptions...)
	_, err := t.Run(cmd)
	return err
}

// CmdContext provides context for command execution
type CmdContext struct {
	// environment variables in k=v format.
	Env   []string
	Dir   string
	Stdin io.Reader
}

// Run executes the provided command within this context
func (cc *CmdContext) Run(cmd *exec.Cmd) ([]byte, error) {
	cmd.Dir = cc.Dir
	cmd.Env = append(os.Environ(), cc.Env...)
	cmd.Stdin = cc.Stdin
	command := strings.Join(cmd.Args, " ")
	_, _ = fmt.Fprintf(GinkgoWriter, "running: %s\n", command)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return output, fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
	}

	return output, nil
}

// AllowProjectBeMultiGroup will update the PROJECT file with the information to allow we scaffold
// apis with different groups. be available.
func (t *TestContext) AllowProjectBeMultiGroup() error {
	const multiGroup = `multigroup: true
`
	projectBytes, err := os.ReadFile(filepath.Join(t.Dir, "PROJECT"))
	if err != nil {
		return fmt.Errorf("cannot read project file: %w", err)
	}

	projectBytes = append([]byte(multiGroup), projectBytes...)
	err = os.WriteFile(filepath.Join(t.Dir, "PROJECT"), projectBytes, 0o644)
	if err != nil {
		return fmt.Errorf("could not write to project file: %w", err)
	}
	return nil
}

// InstallHelm installs Helm in the e2e server.
func (t *TestContext) InstallHelm() error {
	// Check if Helm is already installed
	checkCmd := exec.Command("helm", "version")
	_, err := t.Run(checkCmd)
	if err == nil {
		// Helm is already installed, skip installation
		_, _ = fmt.Fprintf(GinkgoWriter, "Helm is already installed, skipping installation\n")
		return nil
	}

	// Install Helm if not found
	helmInstallScript := "https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4"
	cmd := exec.Command("bash", "-c", fmt.Sprintf("curl -fsSL %s | bash", helmInstallScript))
	_, err = t.Run(cmd)
	if err != nil {
		return err
	}

	verifyCmd := exec.Command("helm", "version")
	_, err = t.Run(verifyCmd)
	if err != nil {
		return err
	}

	return nil
}

// UninstallHelmRelease removes the specified Helm release from the cluster.
// Uses the chart name (project name) as the release name, which is standard Helm practice.
// Note: With crd.keep=true (default), CRDs are preserved after uninstall.
// Use DeleteCRDs() to clean them up after verifying they persisted.
func (t *TestContext) UninstallHelmRelease() error {
	ns := fmt.Sprintf("e2e-%s-system", t.TestSuffix)
	releaseName := fmt.Sprintf("e2e-%s", t.TestSuffix)
	cmd := exec.Command("helm", "uninstall", releaseName, "--namespace", ns)

	_, err := t.Run(cmd)
	if err != nil {
		return err
	}
	return nil
}

// EditHelmPlugin is for running `kubebuilder edit --plugins=helm.kubebuilder.io/v2-alpha`
func (t *TestContext) EditHelmPlugin() error {
	cmd := exec.Command(t.BinaryName, "edit", "--plugins=helm.kubebuilder.io/v2-alpha")
	_, err := t.Run(cmd)
	return err
}

// HelmInstallRelease is for running `helm install`
// Uses the chart name (project name) as the release name, which is standard Helm practice.
// When release name matches chart name, chart.fullname simplifies to just the chart name,
// preserving kustomize resource naming.
func (t *TestContext) HelmInstallRelease() error {
	return t.HelmInstallReleaseWithOptions(true) // Default: crd.keep=true
}

// HelmInstallReleaseWithOptions is for running `helm install` with configurable options
// crdKeep controls whether CRDs are preserved on helm uninstall (helm.sh/resource-policy: keep)
func (t *TestContext) HelmInstallReleaseWithOptions(crdKeep bool) error {
	releaseName := fmt.Sprintf("e2e-%s", t.TestSuffix)
	// Set the image to match what was built (format: e2e-test/controller-manager:suffix)
	// Helm chart uses manager.image.repository and manager.image.tag
	// --create-namespace ensures the namespace exists before installing
	cmd := exec.Command("helm", "install", releaseName, "dist/chart",
		"--namespace", fmt.Sprintf("e2e-%s-system", t.TestSuffix),
		"--create-namespace",
		"--set", fmt.Sprintf("manager.image.repository=%s", "e2e-test/controller-manager"),
		"--set", fmt.Sprintf("manager.image.tag=%s", t.TestSuffix),
		"--set", fmt.Sprintf("crd.keep=%t", crdKeep))
	_, err := t.Run(cmd)
	return err
}

// HelmUpgradeReleaseWithReplicas runs `helm upgrade` with manager.replicas set.
// Uses --reuse-values so existing image and other settings are preserved.
func (t *TestContext) HelmUpgradeReleaseWithReplicas(replicas int) error {
	releaseName := fmt.Sprintf("e2e-%s", t.TestSuffix)
	ns := fmt.Sprintf("e2e-%s-system", t.TestSuffix)
	cmd := exec.Command("helm", "upgrade", releaseName, "dist/chart",
		"--namespace", ns,
		"--reuse-values",
		"--set", fmt.Sprintf("manager.replicas=%d", replicas))
	_, err := t.Run(cmd)
	return err
}


================================================
FILE: test/e2e/utils/webhooks.go
================================================
/*
Copyright 2024 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 utils

import (
	"fmt"
	"os"

	"sigs.k8s.io/kubebuilder/v4/pkg/plugin/util"
)

// ImplementWebhooks will mock an webhook data
func ImplementWebhooks(filename, _ string) error {
	//nolint:gosec // false positive
	bs, err := os.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("error reading webhooks file %q: %w", filename, err)
	}
	str := string(bs)

	str, err = util.EnsureExistAndReplace(
		str,
		"import (",
		`import (
	"errors"`)
	if err != nil {
		return fmt.Errorf("error replacing imports in webhooks file %q: %w", filename, err)
	}

	// implement defaulting webhook logic
	replace := `if obj.Spec.Count == 0 {
		obj.Spec.Count = 5
	}`
	str, err = util.EnsureExistAndReplace(
		str,
		"// TODO(user): fill in your defaulting logic.",
		replace,
	)
	if err != nil {
		return fmt.Errorf("error replacing default logic in webhooks file %q: %w", filename, err)
	}

	// implement validation webhook logic
	str, err = util.EnsureExistAndReplace(
		str,
		"// TODO(user): fill in your validation logic upon object creation.",
		`if obj.Spec.Count < 0 {
		return nil, errors.New(".spec.count must >= 0")
	}`)
	if err != nil {
		return fmt.Errorf("error replacing validation logic in webhooks file %q: %w", filename, err)
	}
	str, err = util.EnsureExistAndReplace(
		str,
		"// TODO(user): fill in your validation logic upon object update.",
		`if newObj.Spec.Count < 0 {
		return nil, errors.New(".spec.count must >= 0")
	}`)
	if err != nil {
		return fmt.Errorf("error replacing validation logic in webhooks file %q: %w", filename, err)
	}
	//nolint:gosec // false positive
	if writeFileErr := os.WriteFile(filename, []byte(str), 0o644); writeFileErr != nil {
		return fmt.Errorf("error writing webhooks file %q: %w", filename, writeFileErr)
	}

	return nil
}


================================================
FILE: test/testdata/check.sh
================================================
#!/usr/bin/env bash

# Copyright 2019 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.

source "$(dirname "$0")/../common.sh"

check_directory="$(dirname "$0")/../../testdata"

# Check testdata directory first. If there are any uncommitted change, fail the test.
if [[ $(git status ${check_directory} --porcelain) ]]; then
  header_text "Generate Testdata test precondition failed!"
  header_text "Please commit the change under testdata directory before running the Generate Testdata test"
  exit 1
fi

$(dirname "$0")/generate.sh

# Check if there are any changes to files under testdata directory.
if [[ $(git status ${check_directory} --porcelain) ]]; then
  git status ${check_directory} --porcelain
  git diff ${check_directory}
  header_text "Generate Testdata failed!"
  header_text "Please, if you have changed the scaffolding make sure you have run: make generate"
  exit 1
else
  header_text "Generate Testdata passed!"
fi


================================================
FILE: test/testdata/generate.sh
================================================
#!/usr/bin/env bash

#  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.

source "$(dirname "$0")/../common.sh"

# This function scaffolds test projects given a project name and flags.
#
# Usage:
#
#   scaffold_test_project   
function scaffold_test_project {
  local project=$1
  shift
  local init_flags="$@"

  local testdata_dir="$(dirname "$0")/../../testdata"
  mkdir -p $testdata_dir/$project
  rm -rf $testdata_dir/$project/*
  pushd $testdata_dir/$project

  header_text "Generating project ${project} with flags: ${init_flags}"
  go mod init sigs.k8s.io/kubebuilder/testdata/$project  # our repo autodetection will traverse up to the kb module if we don't do this
  header_text "Initializing project ..."
  $kb init $init_flags --domain testproject.org --license apache2 --owner "The Kubernetes authors"

  if [ $project == "project-v4" ] ; then
    header_text 'Creating APIs ...'
    $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false
    $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false --force
    $kb create webhook --group crew --version v1 --kind Captain --defaulting --make=false
    $kb create webhook --group crew --version v1 --kind Captain --programmatic-validation --make=false

    # Create API to test conversion from v1 to v2
    $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false
    $kb create api --group crew --version v2 --kind FirstMate --controller=false --resource=true --make=false
    $kb create webhook --group crew --version v1 --kind FirstMate --conversion --make=false --spoke v2

    # Create API with custom webhook paths - split to test incremental with custom paths
    $kb create api --group crew --version v1 --kind Sailor --controller=true --resource=true --make=false
    $kb create webhook --group crew --version v1 --kind Sailor --defaulting --defaulting-path=/custom-mutate-sailor --make=false
    $kb create webhook --group crew --version v1 --kind Sailor --programmatic-validation --validation-path=/custom-validate-sailor --make=false

    $kb create api --group crew --version v1 --kind Admiral --plural=admirales --controller=true --resource=true --namespaced=false --make=false
    # Split to test incremental: defaulting first, then validation with custom path
    $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --defaulting --make=false
    $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --programmatic-validation --validation-path=/custom-validate-admiral --make=false
    # Controller for External types
    $kb create api --group "cert-manager" --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io --external-api-module=github.com/cert-manager/cert-manager@v1.20.0
    # Webhook for External types
    $kb create webhook --group "cert-manager" --version v1 --kind Issuer --defaulting --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io --external-api-module=github.com/cert-manager/cert-manager@v1.20.0
    # Webhook for Core type
    $kb create webhook --group core --version v1 --kind Pod --defaulting
    # Webhook for kubernetes Core type that is part of an api group - test incremental
    $kb create webhook --group apps --version v1 --kind Deployment --defaulting
    $kb create webhook --group apps --version v1 --kind Deployment --programmatic-validation
  fi

  if [[ $project =~ multigroup ]]; then
    header_text 'Creating APIs ...'
    $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false
    # Test incremental webhook additions
    $kb create webhook --group crew --version v1 --kind Captain --defaulting --make=false
    $kb create webhook --group crew --version v1 --kind Captain --programmatic-validation --make=false

    $kb create api --group ship --version v1beta1 --kind Frigate --controller=true --resource=true --make=false
    $kb create api --group ship --version v1 --kind Destroyer --controller=true --resource=true --namespaced=false --make=false
    $kb create webhook --group ship --version v1 --kind Destroyer --defaulting --make=false
    $kb create api --group ship --version v2alpha1 --kind Cruiser --controller=true --resource=true --namespaced=false --make=false
    $kb create webhook --group ship --version v2alpha1 --kind Cruiser --programmatic-validation --make=false

    $kb create api --group sea-creatures --version v1beta1 --kind Kraken --controller=true --resource=true --make=false
    $kb create api --group sea-creatures --version v1beta2 --kind Leviathan --controller=true --resource=true --make=false
    $kb create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true --make=false
    # Controller for Core types
    $kb create api --group apps --version v1 --kind Deployment --controller=true --resource=false --make=false
    $kb create api --group foo --version v1 --kind Bar --controller=true --resource=true --make=false
    $kb create api --group fiz --version v1 --kind Bar --controller=true --resource=true --make=false
    # Controller for External types
    $kb create api --group "cert-manager" --version v1 --kind Certificate --controller=true --resource=false --make=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io --external-api-module=github.com/cert-manager/cert-manager@v1.20.0
    # Webhook for External types
    $kb create webhook --group "cert-manager" --version v1 --kind Issuer --defaulting --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io --external-api-module=github.com/cert-manager/cert-manager@v1.20.0
    # Webhook for Core type
    $kb create webhook --group core --version v1 --kind Pod --programmatic-validation --make=false
    # Webhook for kubernetes Core type that is part of an api group - test incremental
    $kb create webhook --group apps --version v1 --kind Deployment --defaulting --make=false
    $kb create webhook --group apps --version v1 --kind Deployment --programmatic-validation --make=false
  fi

  if [[ $project =~ with-plugins ]] ; then
    header_text 'Enabling namespace-scoped deployment ...'
    # Use edit command to test toggling namespaced mode on existing projects
    # This test is to ensure that we don't break and we still with both flags
    $kb edit --namespaced --force
  fi

  if [[ $project =~ multigroup ]] || [[ $project =~ with-plugins ]] ; then
    header_text 'With Optional Plugins ...'
    header_text 'Creating APIs with deploy-image plugin ...'
    $kb create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:1.6.26-alpine3.19 --image-container-command="memcached,--memory-limit=64,-o,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha" --make=false
    $kb create api --group example.com --version v1alpha1 --kind Busybox --image=busybox:1.36.1 --plugins="deploy-image/v1-alpha" --make=false
    # Create only validation webhook for Memcached
    $kb create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation --make=false
    # Create API to check webhook --conversion from v1 to v2
    $kb create api --group example.com --version v1 --kind Wordpress --controller=true --resource=true  --make=false
    $kb create api --group example.com --version v2 --kind Wordpress --controller=false --resource=true  --make=false
    $kb create webhook --group example.com --version v1 --kind Wordpress --conversion --make=false --spoke v2

    header_text 'Editing project with Grafana plugin ...'
    $kb edit --plugins=grafana.kubebuilder.io/v1-alpha
  fi

  make all
  make build-installer

  if [[ $project =~ with-plugins ]] ; then
    header_text 'Editing project with Helm plugin ...'
    $kb edit --plugins=helm.kubebuilder.io/v2-alpha

    header_text 'Editing project with Auto Update plugin ...'
    $kb edit --plugins=autoupdate.kubebuilder.io/v1-alpha --use-gh-models
  fi

  # To avoid conflicts
  rm -f go.sum
  go mod tidy
  popd
}

build_kb

scaffold_test_project project-v4 --plugins="go/v4"
scaffold_test_project project-v4-multigroup --plugins="go/v4" --multigroup
scaffold_test_project project-v4-with-plugins --plugins="go/v4" --namespaced


================================================
FILE: test/testdata/legacy-webhook-path.sh
================================================
#!/usr/bin/env bash

#  Copyright 2024 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.

##############################
# TODO: Remove me when go/v4 is no longer supported
# This script i used to validate the legacy webhook path
##############################

source "$(dirname "$0")/../common.sh"

# This function scaffolds test projects given a project name and flags.
#
# Usage:
#
#   scaffold_test_project   
function scaffold_test_project {
  local project=$1
  shift
  local init_flags="$@"

  local testdata_dir="$(dirname "$0")/../../testdata"
  mkdir -p $testdata_dir/$project
  rm -rf $testdata_dir/$project/*
  pushd $testdata_dir/$project

  header_text "Generating project ${project} with flags: ${init_flags}"
  go mod init sigs.k8s.io/kubebuilder/testdata/$project  # our repo autodetection will traverse up to the kb module if we don't do this
  header_text "Initializing project ..."
  $kb init $init_flags --domain testproject.org --license apache2 --owner "The Kubernetes authors"

  if [ $project == "legacy-project-v4" ] ; then
    header_text 'Creating APIs ...'
    $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false
    $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false --force
    $kb create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation --legacy=true
    $kb create api --group crew --version v1 --kind FirstMate --controller=true --resource=true --make=false
    $kb create api --group crew --version v2 --kind FirstMate --controller=false --resource=true --make=false
    $kb create webhook --group crew --version v1 --kind FirstMate --conversion --spoke v2 --legacy=true --make=false
    $kb create api --group crew --version v1 --kind Admiral --plural=admirales --controller=true --resource=true --namespaced=false --make=false
    $kb create webhook --group crew --version v1 --kind Admiral --plural=admirales --defaulting --legacy=true
  fi

  if [[ $project =~ multigroup ]]; then
    header_text 'Creating APIs ...'
    $kb create api --group crew --version v1 --kind Captain --controller=true --resource=true --make=false
    $kb create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation --legacy=true

    $kb create api --group ship --version v1beta1 --kind Frigate --controller=true --resource=true --make=false
    $kb create api --group ship --version v1 --kind Frigate --controller=false --resource=true --make=false
    $kb create webhook --group ship --version v1beta1 --kind Frigate --conversion --spoke v1 --legacy=true

    $kb create api --group ship --version v1 --kind Destroyer --controller=true --resource=true --namespaced=false --make=false
    $kb create webhook --group ship --version v1 --kind Destroyer --defaulting --legacy=true
    $kb create api --group ship --version v2alpha1 --kind Cruiser --controller=true --resource=true --namespaced=false --make=false
    $kb create webhook --group ship --version v2alpha1 --kind Cruiser --programmatic-validation --legacy=true

    $kb create api --group sea-creatures --version v1beta1 --kind Kraken --controller=true --resource=true --make=false
    $kb create api --group sea-creatures --version v1beta2 --kind Leviathan --controller=true --resource=true --make=false
    $kb create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true --make=false
    $kb create api --group apps --version v1 --kind Deployment --controller=true --resource=false --make=false
    $kb create api --group foo --version v1 --kind Bar --controller=true --resource=true --make=false
    $kb create api --group fiz --version v1 --kind Bar --controller=true --resource=true --make=false
  fi

  if [[ $project =~ multigroup ]] || [[ $project =~ with-plugins ]] ; then
    header_text 'With Optional Plugins ...'
    header_text 'Creating APIs with deploy-image plugin ...'
    $kb create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:memcached:1.6.26-alpine3.19 --image-container-command="memcached,--memory-limit=64,-o,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha" --make=false
    $kb create api --group example.com --version v1alpha1 --kind Busybox --image=busybox:1.36.1 --plugins="deploy-image/v1-alpha" --make=false
    $kb create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation --legacy=true
    header_text 'Editing project with Grafana plugin ...'
    $kb edit --plugins=grafana.kubebuilder.io/v1-alpha
  fi

  make all
  make build-installer
  go mod tidy
  make test
  popd
}

build_kb

scaffold_test_project legacy-project-v4 --plugins="go/v4"
scaffold_test_project legacy-project-v4-multigroup --plugins="go/v4" --multigroup
scaffold_test_project legacy-project-v4-with-plugins --plugins="go/v4"


================================================
FILE: test/testdata/test.sh
================================================
#!/usr/bin/env bash

# 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.

source "$(dirname "$0")/../common.sh"

# Executes the test of the testdata directories
function test_project {
  rm -f "$(command -v controller-gen)"
  rm -f "$(command -v kustomize)"

  header_text "Performing tests in dir $1"
  pushd "$(dirname "$0")/../../testdata/$1"
  go mod tidy
  make test
  popd
}

build_kb

# Project version v4-alpha
test_project project-v4
test_project project-v4-multigroup
test_project project-v4-with-plugins


================================================
FILE: test/testdata/test_legacy.sh
================================================
#!/usr/bin/env bash
# todo: remove this file when go/v2 be removed
# 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.

source "$(dirname "$0")/../common.sh"

# Executes the test of the testdata directories
function test_project {
  rm -f "$(command -v controller-gen)"
  rm -f "$(command -v kustomize)"

  header_text "Performing tests in dir $1"
  pushd "$(dirname "$0")/../../testdata/$1"
  go mod tidy
  go get -u golang.org/x/sys
  make test
  popd
}

build_kb

# Test project v2, which relies on pre-installed envtest tools to run 'make test'.
tools_k8s_version="1.19.2"
fetch_tools
test_project project-v2
test_project project-v3


================================================
FILE: test.sh
================================================
#!/usr/bin/env bash

# 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.

# Source common.sh for environment setup (PATH, GOBIN, etc.)
source "$(dirname "$0")/test/common.sh"

# Build kubebuilder binary
build_kb
fetch_tools

pushd . >/dev/null

make test

header_text "All tests passed!"

popd >/dev/null


================================================
FILE: test_e2e.sh
================================================
#!/usr/bin/env bash

# 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.

./test/e2e/ci.sh


================================================
FILE: testdata/project-v4/.custom-gcl.yml
================================================
# This file configures golangci-lint with module plugins.
# When you run 'make lint', it will automatically build a custom golangci-lint binary
# with all the plugins listed below.
#
# See: https://golangci-lint.run/plugins/module-plugins/
version: v2.8.0
plugins:
  # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs)
  - module: "sigs.k8s.io/logtools"
    import: "sigs.k8s.io/logtools/logcheck/gclplugin"
    version: latest


================================================
FILE: testdata/project-v4/.devcontainer/devcontainer.json
================================================
{
  "name": "Kubebuilder DevContainer",
  "image": "golang:1.25",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {
      "moby": false,
      "dockerDefaultAddressPool": "base=172.30.0.0/16,size=24"
    },
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/common-utils:2": {
      "upgradePackages": true
    }
  },

  "runArgs": ["--privileged", "--init"],

  "customizations": {
    "vscode": {
      "settings": {
        "terminal.integrated.shell.linux": "/bin/bash"
      },
      "extensions": [
        "ms-kubernetes-tools.vscode-kubernetes-tools",
        "ms-azuretools.vscode-docker"
      ]
    }
  },

  "remoteEnv": {
    "GO111MODULE": "on"
  },

  "onCreateCommand": "bash .devcontainer/post-install.sh"
}



================================================
FILE: testdata/project-v4/.devcontainer/post-install.sh
================================================
#!/bin/bash
set -euo pipefail

echo "===================================="
echo "Kubebuilder DevContainer Setup"
echo "===================================="

# Verify running as root (required for installing to /usr/local/bin and /etc)
if [ "$(id -u)" -ne 0 ]; then
  echo "ERROR: This script must be run as root"
  exit 1
fi

echo ""
echo "Detecting system architecture..."
# Detect architecture using uname
MACHINE=$(uname -m)
case "${MACHINE}" in
  x86_64)
    ARCH="amd64"
    ;;
  aarch64|arm64)
    ARCH="arm64"
    ;;
  *)
    echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64"
    ARCH="amd64"
    ;;
esac
echo "Architecture: ${ARCH}"

echo ""
echo "------------------------------------"
echo "Setting up bash completion..."
echo "------------------------------------"

BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions"

# Enable bash-completion in root's .bashrc (devcontainer runs as root)
if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then
  echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc
  echo "Added bash-completion to .bashrc"
fi

echo ""
echo "------------------------------------"
echo "Installing development tools..."
echo "------------------------------------"

# Install kind
if ! command -v kind &> /dev/null; then
  echo "Installing kind..."
  curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}"
  chmod +x /usr/local/bin/kind
  echo "kind installed successfully"
fi

# Generate kind bash completion
if command -v kind &> /dev/null; then
  if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then
    echo "kind completion installed"
  else
    echo "WARNING: Failed to generate kind completion"
  fi
fi

# Install kubebuilder
if ! command -v kubebuilder &> /dev/null; then
  echo "Installing kubebuilder..."
  curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}"
  chmod +x /usr/local/bin/kubebuilder
  echo "kubebuilder installed successfully"
fi

# Generate kubebuilder bash completion
if command -v kubebuilder &> /dev/null; then
  if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then
    echo "kubebuilder completion installed"
  else
    echo "WARNING: Failed to generate kubebuilder completion"
  fi
fi

# Install kubectl
if ! command -v kubectl &> /dev/null; then
  echo "Installing kubectl..."
  KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt)
  curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl"
  chmod +x /usr/local/bin/kubectl
  echo "kubectl installed successfully"
fi

# Generate kubectl bash completion
if command -v kubectl &> /dev/null; then
  if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then
    echo "kubectl completion installed"
  else
    echo "WARNING: Failed to generate kubectl completion"
  fi
fi

# Generate Docker bash completion
if command -v docker &> /dev/null; then
  if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then
    echo "docker completion installed"
  else
    echo "WARNING: Failed to generate docker completion"
  fi
fi

echo ""
echo "------------------------------------"
echo "Configuring Docker environment..."
echo "------------------------------------"

# Wait for Docker to be ready
echo "Waiting for Docker to be ready..."
for i in {1..30}; do
  if docker info >/dev/null 2>&1; then
    echo "Docker is ready"
    break
  fi
  if [ "$i" -eq 30 ]; then
    echo "WARNING: Docker not ready after 30s"
  fi
  sleep 1
done

# Create kind network (ignore if already exists)
if ! docker network inspect kind >/dev/null 2>&1; then
  if docker network create kind >/dev/null 2>&1; then
    echo "Created kind network"
  else
    echo "WARNING: Failed to create kind network (may already exist)"
  fi
fi

echo ""
echo "------------------------------------"
echo "Verifying installations..."
echo "------------------------------------"
kind version
kubebuilder version
kubectl version --client
docker --version
go version

echo ""
echo "===================================="
echo "DevContainer ready!"
echo "===================================="
echo "All development tools installed successfully."
echo "You can now start building Kubernetes operators."


================================================
FILE: testdata/project-v4/.dockerignore
================================================
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore everything by default and re-include only needed files
**

# Re-include Go source files (but not *_test.go)
!**/*.go
**/*_test.go

# Re-include Go module files
!go.mod
!go.sum


================================================
FILE: testdata/project-v4/.github/workflows/lint.yml
================================================
name: Lint

on:
  push:
  pull_request:

jobs:
  lint:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Check linter configuration
        run: make lint-config
      - name: Run linter
        run: make lint


================================================
FILE: testdata/project-v4/.github/workflows/test-e2e.yml
================================================
name: E2E Tests

on:
  push:
  pull_request:

jobs:
  test-e2e:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install the latest version of kind
        run: |
          curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind

      - name: Verify kind installation
        run: kind version

      - name: Running Test e2e
        run: |
          go mod tidy
          make test-e2e


================================================
FILE: testdata/project-v4/.github/workflows/test.yml
================================================
name: Tests

on:
  push:
  pull_request:

jobs:
  test:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Running Tests
        run: |
          go mod tidy
          make test


================================================
FILE: testdata/project-v4/.gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
Dockerfile.cross

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Go workspace file
go.work

# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*

# editor and IDE paraphernalia
.idea
.vscode
*.swp
*.swo
*~

# Kubeconfig might contain secrets
*.kubeconfig


================================================
FILE: testdata/project-v4/.golangci.yml
================================================
version: "2"
run:
  allow-parallel-runners: true
linters:
  default: none
  enable:
    - copyloopvar
    - dupl
    - errcheck
    - ginkgolinter
    - goconst
    - gocyclo
    - govet
    - ineffassign
    - lll
    - modernize
    - misspell
    - nakedret
    - prealloc
    - revive
    - staticcheck
    - unconvert
    - unparam
    - unused
    - logcheck
  settings:
    custom:
      logcheck:
        type: "module"
        description: Checks Go logging calls for Kubernetes logging conventions.
    revive:
      rules:
        - name: comment-spacings
        - name: import-shadowing
    modernize:
      disable:
        - omitzero
  exclusions:
    generated: lax
    rules:
      - linters:
          - lll
        path: api/*
      - linters:
          - dupl
          - lll
        path: internal/*
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gofmt
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$


================================================
FILE: testdata/project-v4/AGENTS.md
================================================
# project-v4 - AI Agent Guide

## Project Structure

**Single-group layout (default):**
```
cmd/main.go                    Manager entry (registers controllers/webhooks)
api//*_types.go       CRD schemas (+kubebuilder markers)
api//zz_generated.*   Auto-generated (DO NOT EDIT)
internal/controller/*          Reconciliation logic
internal/webhook/*             Validation/defaulting (if present)
config/crd/bases/*             Generated CRDs (DO NOT EDIT)
config/rbac/role.yaml          Generated RBAC (DO NOT EDIT)
config/samples/*               Example CRs (edit these)
Makefile                       Build/test/deploy commands
PROJECT                        Kubebuilder metadata Auto-generated (DO NOT EDIT)
```

**Multi-group layout** (for projects with multiple API groups):
```
api///*_types.go       CRD schemas by group
internal/controller//*          Controllers by group
internal/webhook///*   Webhooks by group and version (if present)
```

Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`.

**To convert to multi-group layout:**
1. Run: `kubebuilder edit --multigroup=true`
2. Move APIs: `mkdir -p api/ && mv api/ api//`
3. Move controllers: `mkdir -p internal/controller/ && mv internal/controller/*.go internal/controller//`
4. Move webhooks (if present): `mkdir -p internal/webhook/ && mv internal/webhook/ internal/webhook//`
5. Update import paths in all files
6. Fix `path` in `PROJECT` file for each resource
7. Update test suite CRD paths (add one more `..` to relative paths)

## Critical Rules

### Never Edit These (Auto-Generated)
- `config/crd/bases/*.yaml` - from `make manifests`
- `config/rbac/role.yaml` - from `make manifests`
- `config/webhook/manifests.yaml` - from `make manifests`
- `**/zz_generated.*.go` - from `make generate`
- `PROJECT` - from `kubebuilder [OPTIONS]`

### Never Remove Scaffold Markers
Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers.

### Keep Project Structure
Do not move files around. The CLI expects files in specific locations.

### Always Use CLI Commands
Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually.

### E2E Tests Require an Isolated Kind Cluster
The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI).
Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster).

## After Making Changes

**After editing `*_types.go` or markers:**
```
make manifests  # Regenerate CRDs/RBAC from markers
make generate   # Regenerate DeepCopy methods
```

**After editing `*.go` files:**
```
make lint-fix   # Auto-fix code style
make test       # Run unit tests
```

## CLI Commands Cheat Sheet

### Create API (your own types)
```bash
kubebuilder create api --group  --version  --kind 
```

### Deploy Image Plugin (scaffold to deploy/manage ANY container image)

Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.):

```bash
# Example: deploying memcached
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \
  --image=memcached:alpine \
  --plugins=deploy-image.go.kubebuilder.io/v1-alpha
```

Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation.


### Create Webhooks
```bash
# Validation + defaulting
kubebuilder create webhook --group  --version  --kind  \
  --defaulting --programmatic-validation

# Conversion webhook (for multi-version APIs)
kubebuilder create webhook --group  --version v1 --kind  \
  --conversion --spoke v2
```

### Controller for Core Kubernetes Types
```bash
# Watch Pods
kubebuilder create api --group core --version v1 --kind Pod \
  --controller=true --resource=false

# Watch Deployments
kubebuilder create api --group apps --version v1 --kind Deployment \
  --controller=true --resource=false
```

### Controller for External Types (e.g., from other operators)

Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.):

```bash
# Example: watching cert-manager Certificate resources
kubebuilder create api \
  --group cert-manager --version v1 --kind Certificate \
  --controller=true --resource=false \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
```

**Note:** Use `--external-api-module=@` only if you need a specific version. Otherwise, omit `@` to use what's in go.mod.

### Webhook for External Types

```bash
# Example: validating external resources
kubebuilder create webhook \
  --group cert-manager --version v1 --kind Issuer \
  --defaulting \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
```

## Testing & Development

```bash
make test              # Run unit tests (uses envtest: real K8s API + etcd)
make run               # Run locally (uses current kubeconfig context)
```

Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup.

## Deployment Workflow

```bash
# 1. Regenerate manifests
make manifests generate

# 2. Build & deploy
export IMG=/:tag
make docker-build docker-push IMG=$IMG  # Or: kind load docker-image $IMG --name 
make deploy IMG=$IMG

# 3. Test
kubectl apply -k config/samples/

# 4. Debug
kubectl logs -n -system deployment/-controller-manager -c manager -f
```

### API Design

**Key markers for** `api//*_types.go`:

```go
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"

// On fields:
// +kubebuilder:validation:Required
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:MaxLength=100
// +kubebuilder:validation:Pattern="^[a-z]+$"
// +kubebuilder:default="value"
```

- **Use** `metav1.Condition` for status (not custom string fields)
- **Use predefined types**: `metav1.Time` instead of `string` for dates
- **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`)

### Controller Design

**RBAC markers in** `internal/controller/*_controller.go`:

```go
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
```

**Implementation rules:**
- **Idempotent reconciliation**: Safe to run multiple times
- **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts
- **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)`
- **Owner references**: Enable automatic garbage collection (`SetControllerReference`)
- **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter`
- **Finalizers**: Clean up external resources (buckets, VMs, DNS entries)

### Logging

**Follow Kubernetes logging message style guidelines:**

- Start from a capital letter
- Do not end the message with a period
- Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`)
- Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"`
- Specify object type: `"Deleted Pod"` not `"Deleted"`
- Balanced key-value pairs

```go
log.Info("Starting reconciliation")
log.Info("Created Deployment", "name", deploy.Name)
log.Error(err, "Failed to create Pod", "name", name)
```

**Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines

### Webhooks
- **Create all types together**: `--defaulting --programmatic-validation --conversion`
- **When`--force`is used**: Backup custom logic first, then restore after scaffolding
- **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`)
  - Hub version: Usually oldest stable version (v1)
  - Spoke versions: Newer versions that convert to/from hub (v2, v3)
  - Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke)

### Learning from Examples

The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation:

```bash
kubebuilder create api --group example --version v1alpha1 --kind MyApp \
  --image= --plugins=deploy-image.go.kubebuilder.io/v1-alpha
```

Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation.

## Distribution Options

### Option 1: YAML Bundle (Kustomize)

```bash
# Generate dist/install.yaml from Kustomize manifests
make build-installer IMG=/:tag
```

**Key points:**
- The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment)
- Commit this file to your repository for easy distribution
- Users only need `kubectl` to install (no additional tools required)

**Example:** Users install with a single command:
```bash
kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml
```

### Option 2: Helm Chart

```bash
kubebuilder edit --plugins=helm/v2-alpha                      # Generates dist/chart/ (default)
kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts  # Generates charts/chart/
```

**For development:**
```bash
make helm-deploy IMG=/:          # Deploy manager via Helm
make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..."    # Deploy with custom values
make helm-status                                         # Show release status
make helm-uninstall                                      # Remove release
make helm-history                                        # View release history
make helm-rollback                                       # Rollback to previous version
```

**For end users/production:**
```bash
helm install my-release .//chart/ --namespace  --create-namespace
```

**Important:** If you add webhooks or modify manifests after initial chart generation:
1. Backup any customizations in `/chart/values.yaml` and `/chart/manager/manager.yaml`
2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized)
3. Manually restore your custom values from the backup

### Publish Container Image

```bash
export IMG=/:
make docker-build docker-push IMG=$IMG
```

## References

### Essential Reading
- **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide)
- **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions)
- **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.)
- **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels)

### API Design & Implementation
- **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- **Markers Reference**: https://book.kubebuilder.io/reference/markers.html

### Tools & Libraries
- **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime
- **controller-tools**: https://github.com/kubernetes-sigs/controller-tools
- **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder


================================================
FILE: testdata/project-v4/Dockerfile
================================================
# Build the manager binary
FROM golang:1.25 AS builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

# Copy the Go source (relies on .dockerignore to filter)
COPY . .

# Build
# the GOARCH has no default value to allow the binary to be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]


================================================
FILE: testdata/project-v4/Makefile
================================================
# Image URL to use all building/pushing image targets
IMG ?= controller:latest

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

# CONTAINER_TOOL defines the container tool to be used for building images.
# Be aware that the target commands are only tested with Docker which is
# scaffolded by default. However, you might want to replace it to use other
# tools. (i.e. podman)
CONTAINER_TOOL ?= docker

# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

.PHONY: all
all: build

##@ General

# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk command is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php

.PHONY: help
help: ## Display this help.
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Development

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
	"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."

.PHONY: fmt
fmt: ## Run go fmt against code.
	go fmt ./...

.PHONY: vet
vet: ## Run go vet against code.
	go vet ./...

.PHONY: test
test: manifests generate fmt vet setup-envtest ## Run tests.
	KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out

# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
# CertManager is installed by default; skip with:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= project-v4-test-e2e

.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
	@command -v $(KIND) >/dev/null 2>&1 || { \
		echo "Kind is not installed. Please install Kind manually."; \
		exit 1; \
	}
	@case "$$($(KIND) get clusters)" in \
		*"$(KIND_CLUSTER)"*) \
			echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
		*) \
			echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
			$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
	esac

.PHONY: test-e2e
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
	KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
	$(MAKE) cleanup-test-e2e

.PHONY: cleanup-test-e2e
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
	@$(KIND) delete cluster --name $(KIND_CLUSTER)

.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
	"$(GOLANGCI_LINT)" run

.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
	"$(GOLANGCI_LINT)" run --fix

.PHONY: lint-config
lint-config: golangci-lint ## Verify golangci-lint linter configuration
	"$(GOLANGCI_LINT)" config verify

##@ Build

.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
	go build -o bin/manager cmd/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
	go run ./cmd/main.go

# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
	$(CONTAINER_TOOL) build -t ${IMG} .

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
	$(CONTAINER_TOOL) push ${IMG}

# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail)
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: ## Build and push docker image for the manager for cross-platform support
	# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
	sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
	- $(CONTAINER_TOOL) buildx create --name project-v4-builder
	$(CONTAINER_TOOL) buildx use project-v4-builder
	- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
	- $(CONTAINER_TOOL) buildx rm project-v4-builder
	rm Dockerfile.cross

.PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
	mkdir -p dist
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default > dist/install.yaml

##@ Deployment

ifndef ignore-not-found
  ignore-not-found = false
endif

.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi

.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi

.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -

.PHONY: undeploy
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -

##@ Dependencies

## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
	mkdir -p "$(LOCALBIN)"

## Tool Binaries
KUBECTL ?= kubectl
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint

## Tool Versions
KUSTOMIZE_VERSION ?= v5.8.1
CONTROLLER_TOOLS_VERSION ?= v0.20.1

#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/')

#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')

GOLANGCI_LINT_VERSION ?= v2.8.0
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
	$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))

.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
	$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))

.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
	@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
	@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
		echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
		exit 1; \
	}

.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
	$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
	$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
	@test -f .custom-gcl.yml && { \
		echo "Building custom golangci-lint with plugins..." && \
		$(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \
		mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \
	} || true

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f "$(1)" ;\
GOBIN="$(LOCALBIN)" go install $${package} ;\
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
} ;\
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
endef

define gomodver
$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
endef


================================================
FILE: testdata/project-v4/PROJECT
================================================
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
cliVersion: (devel)
domain: testproject.org
layout:
- go.kubebuilder.io/v4
projectName: project-v4
repo: sigs.k8s.io/kubebuilder/testdata/project-v4
resources:
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: crew
  kind: Captain
  path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1
  version: v1
  webhooks:
    defaulting: true
    validation: true
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: crew
  kind: FirstMate
  path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1
  version: v1
  webhooks:
    conversion: true
    spoke:
    - v2
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  domain: testproject.org
  group: crew
  kind: FirstMate
  path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v2
  version: v2
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: crew
  kind: Sailor
  path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1
  version: v1
  webhooks:
    defaulting: true
    defaultingPath: /custom-mutate-sailor
    validation: true
    validationPath: /custom-validate-sailor
    webhookVersion: v1
- api:
    crdVersion: v1
  controller: true
  domain: testproject.org
  group: crew
  kind: Admiral
  path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1
  plural: admirales
  version: v1
  webhooks:
    defaulting: true
    validation: true
    validationPath: /custom-validate-admiral
    webhookVersion: v1
- controller: true
  domain: io
  external: true
  group: cert-manager
  kind: Certificate
  module: github.com/cert-manager/cert-manager@v1.20.0
  path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1
  version: v1
- domain: io
  external: true
  group: cert-manager
  kind: Issuer
  module: github.com/cert-manager/cert-manager@v1.20.0
  path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1
  version: v1
  webhooks:
    defaulting: true
    webhookVersion: v1
- core: true
  group: core
  kind: Pod
  path: k8s.io/api/core/v1
  version: v1
  webhooks:
    defaulting: true
    webhookVersion: v1
- core: true
  group: apps
  kind: Deployment
  path: k8s.io/api/apps/v1
  version: v1
  webhooks:
    defaulting: true
    validation: true
    webhookVersion: v1
version: "3"


================================================
FILE: testdata/project-v4/README.md
================================================
# project-v4
// TODO(user): Add simple overview of use/purpose

## Description
// TODO(user): An in-depth paragraph about your project and overview of use

## Getting Started

### Prerequisites
- go version v1.24.6+
- docker version 17.03+.
- kubectl version v1.11.3+.
- Access to a Kubernetes v1.11.3+ cluster.

### To Deploy on the cluster
**Build and push your image to the location specified by `IMG`:**

```sh
make docker-build docker-push IMG=/project-v4:tag
```

**NOTE:** This image ought to be published in the personal registry you specified.
And it is required to have access to pull the image from the working environment.
Make sure you have the proper permission to the registry if the above commands don’t work.

**Install the CRDs into the cluster:**

```sh
make install
```

**Deploy the Manager to the cluster with the image specified by `IMG`:**

```sh
make deploy IMG=/project-v4:tag
```

> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
privileges or be logged in as admin.

**Create instances of your solution**
You can apply the samples (examples) from the config/sample:

```sh
kubectl apply -k config/samples/
```

>**NOTE**: Ensure that the samples has default values to test it out.

### To Uninstall
**Delete the instances (CRs) from the cluster:**

```sh
kubectl delete -k config/samples/
```

**Delete the APIs(CRDs) from the cluster:**

```sh
make uninstall
```

**UnDeploy the controller from the cluster:**

```sh
make undeploy
```

## Project Distribution

Following the options to release and provide this solution to the users.

### By providing a bundle with all YAML files

1. Build the installer for the image built and published in the registry:

```sh
make build-installer IMG=/project-v4:tag
```

**NOTE:** The makefile target mentioned above generates an 'install.yaml'
file in the dist directory. This file contains all the resources built
with Kustomize, which are necessary to install this project without its
dependencies.

2. Using the installer

Users can just run 'kubectl apply -f ' to install
the project, i.e.:

```sh
kubectl apply -f https://raw.githubusercontent.com//project-v4//dist/install.yaml
```

### By providing a Helm Chart

1. Build the chart using the optional helm plugin

```sh
kubebuilder edit --plugins=helm/v2-alpha
```

2. See that a chart was generated under 'dist/chart', and users
can obtain this solution from there.

**NOTE:** If you change the project, you need to update the Helm Chart
using the same command above to sync the latest changes. Furthermore,
if you create webhooks, you need to use the above command with
the '--force' flag and manually ensure that any custom configuration
previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml'
is manually re-applied afterwards.

## Contributing
// TODO(user): Add detailed information on how you would like others to contribute to this project

**NOTE:** Run `make help` for more information on all potential `make` targets

More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)

## License

Copyright 2026 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: testdata/project-v4/api/v1/admiral_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// AdmiralSpec defines the desired state of Admiral
type AdmiralSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Admiral. Edit admiral_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// AdmiralStatus defines the observed state of Admiral.
type AdmiralStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Admiral resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:path=admirales,scope=Cluster

// Admiral is the Schema for the admirales API
type Admiral struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Admiral
	// +required
	Spec AdmiralSpec `json:"spec"`

	// status defines the observed state of Admiral
	// +optional
	Status AdmiralStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// AdmiralList contains a list of Admiral
type AdmiralList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Admiral `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Admiral{}, &AdmiralList{})
}


================================================
FILE: testdata/project-v4/api/v1/captain_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// CaptainSpec defines the desired state of Captain
type CaptainSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Captain. Edit captain_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// CaptainStatus defines the observed state of Captain.
type CaptainStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Captain resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Captain is the Schema for the captains API
type Captain struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Captain
	// +required
	Spec CaptainSpec `json:"spec"`

	// status defines the observed state of Captain
	// +optional
	Status CaptainStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// CaptainList contains a list of Captain
type CaptainList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Captain `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Captain{}, &CaptainList{})
}


================================================
FILE: testdata/project-v4/api/v1/firstmate_conversion.go
================================================
/*
Copyright 2026 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 v1

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// Hub marks this type as a conversion hub.
func (*FirstMate) Hub() {}


================================================
FILE: testdata/project-v4/api/v1/firstmate_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// FirstMateSpec defines the desired state of FirstMate
type FirstMateSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of FirstMate. Edit firstmate_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// FirstMateStatus defines the observed state of FirstMate.
type FirstMateStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the FirstMate resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status

// FirstMate is the Schema for the firstmates API
type FirstMate struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of FirstMate
	// +required
	Spec FirstMateSpec `json:"spec"`

	// status defines the observed state of FirstMate
	// +optional
	Status FirstMateStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// FirstMateList contains a list of FirstMate
type FirstMateList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []FirstMate `json:"items"`
}

func init() {
	SchemeBuilder.Register(&FirstMate{}, &FirstMateList{})
}


================================================
FILE: testdata/project-v4/api/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the crew v1 API group.
// +kubebuilder:object:generate=true
// +groupName=crew.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "crew.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4/api/v1/sailor_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// SailorSpec defines the desired state of Sailor
type SailorSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Sailor. Edit sailor_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// SailorStatus defines the observed state of Sailor.
type SailorStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Sailor resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Sailor is the Schema for the sailors API
type Sailor struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Sailor
	// +required
	Spec SailorSpec `json:"spec"`

	// status defines the observed state of Sailor
	// +optional
	Status SailorStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// SailorList contains a list of Sailor
type SailorList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Sailor `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Sailor{}, &SailorList{})
}


================================================
FILE: testdata/project-v4/api/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Admiral) DeepCopyInto(out *Admiral) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Admiral.
func (in *Admiral) DeepCopy() *Admiral {
	if in == nil {
		return nil
	}
	out := new(Admiral)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Admiral) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdmiralList) DeepCopyInto(out *AdmiralList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Admiral, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmiralList.
func (in *AdmiralList) DeepCopy() *AdmiralList {
	if in == nil {
		return nil
	}
	out := new(AdmiralList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *AdmiralList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdmiralSpec) DeepCopyInto(out *AdmiralSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmiralSpec.
func (in *AdmiralSpec) DeepCopy() *AdmiralSpec {
	if in == nil {
		return nil
	}
	out := new(AdmiralSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *AdmiralStatus) DeepCopyInto(out *AdmiralStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmiralStatus.
func (in *AdmiralStatus) DeepCopy() *AdmiralStatus {
	if in == nil {
		return nil
	}
	out := new(AdmiralStatus)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Captain) DeepCopyInto(out *Captain) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Captain.
func (in *Captain) DeepCopy() *Captain {
	if in == nil {
		return nil
	}
	out := new(Captain)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Captain) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CaptainList) DeepCopyInto(out *CaptainList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Captain, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptainList.
func (in *CaptainList) DeepCopy() *CaptainList {
	if in == nil {
		return nil
	}
	out := new(CaptainList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CaptainList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CaptainSpec) DeepCopyInto(out *CaptainSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptainSpec.
func (in *CaptainSpec) DeepCopy() *CaptainSpec {
	if in == nil {
		return nil
	}
	out := new(CaptainSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CaptainStatus) DeepCopyInto(out *CaptainStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptainStatus.
func (in *CaptainStatus) DeepCopy() *CaptainStatus {
	if in == nil {
		return nil
	}
	out := new(CaptainStatus)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMate) DeepCopyInto(out *FirstMate) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate.
func (in *FirstMate) DeepCopy() *FirstMate {
	if in == nil {
		return nil
	}
	out := new(FirstMate)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FirstMate) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMateList) DeepCopyInto(out *FirstMateList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]FirstMate, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateList.
func (in *FirstMateList) DeepCopy() *FirstMateList {
	if in == nil {
		return nil
	}
	out := new(FirstMateList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FirstMateList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec.
func (in *FirstMateSpec) DeepCopy() *FirstMateSpec {
	if in == nil {
		return nil
	}
	out := new(FirstMateSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus.
func (in *FirstMateStatus) DeepCopy() *FirstMateStatus {
	if in == nil {
		return nil
	}
	out := new(FirstMateStatus)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Sailor) DeepCopyInto(out *Sailor) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sailor.
func (in *Sailor) DeepCopy() *Sailor {
	if in == nil {
		return nil
	}
	out := new(Sailor)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Sailor) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SailorList) DeepCopyInto(out *SailorList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Sailor, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SailorList.
func (in *SailorList) DeepCopy() *SailorList {
	if in == nil {
		return nil
	}
	out := new(SailorList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *SailorList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SailorSpec) DeepCopyInto(out *SailorSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SailorSpec.
func (in *SailorSpec) DeepCopy() *SailorSpec {
	if in == nil {
		return nil
	}
	out := new(SailorSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SailorStatus) DeepCopyInto(out *SailorStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SailorStatus.
func (in *SailorStatus) DeepCopy() *SailorStatus {
	if in == nil {
		return nil
	}
	out := new(SailorStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4/api/v2/firstmate_conversion.go
================================================
/*
Copyright 2026 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 v2

import (
	"log"

	"sigs.k8s.io/controller-runtime/pkg/conversion"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

// ConvertTo converts this FirstMate (v2) to the Hub version (v1).
func (src *FirstMate) ConvertTo(dstRaw conversion.Hub) error {
	dst := dstRaw.(*crewv1.FirstMate)
	log.Printf("ConvertTo: Converting FirstMate from Spoke version v2 to Hub version v1;"+
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)

	// TODO(user): Implement conversion logic from v2 to v1
	// Example: Copying Spec fields
	// dst.Spec.Size = src.Spec.Replicas

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}

// ConvertFrom converts the Hub version (v1) to this FirstMate (v2).
func (dst *FirstMate) ConvertFrom(srcRaw conversion.Hub) error {
	src := srcRaw.(*crewv1.FirstMate)
	log.Printf("ConvertFrom: Converting FirstMate from Hub version v1 to Spoke version v2;"+
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)

	// TODO(user): Implement conversion logic from v1 to v2
	// Example: Copying Spec fields
	// dst.Spec.Replicas = src.Spec.Size

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}


================================================
FILE: testdata/project-v4/api/v2/firstmate_types.go
================================================
/*
Copyright 2026 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 v2

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// FirstMateSpec defines the desired state of FirstMate
type FirstMateSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of FirstMate. Edit firstmate_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// FirstMateStatus defines the observed state of FirstMate.
type FirstMateStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the FirstMate resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// FirstMate is the Schema for the firstmates API
type FirstMate struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of FirstMate
	// +required
	Spec FirstMateSpec `json:"spec"`

	// status defines the observed state of FirstMate
	// +optional
	Status FirstMateStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// FirstMateList contains a list of FirstMate
type FirstMateList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []FirstMate `json:"items"`
}

func init() {
	SchemeBuilder.Register(&FirstMate{}, &FirstMateList{})
}


================================================
FILE: testdata/project-v4/api/v2/groupversion_info.go
================================================
/*
Copyright 2026 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 v2 contains API Schema definitions for the crew v2 API group.
// +kubebuilder:object:generate=true
// +groupName=crew.testproject.org
package v2

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "crew.testproject.org", Version: "v2"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4/api/v2/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v2

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMate) DeepCopyInto(out *FirstMate) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMate.
func (in *FirstMate) DeepCopy() *FirstMate {
	if in == nil {
		return nil
	}
	out := new(FirstMate)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FirstMate) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMateList) DeepCopyInto(out *FirstMateList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]FirstMate, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateList.
func (in *FirstMateList) DeepCopy() *FirstMateList {
	if in == nil {
		return nil
	}
	out := new(FirstMateList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FirstMateList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMateSpec) DeepCopyInto(out *FirstMateSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateSpec.
func (in *FirstMateSpec) DeepCopy() *FirstMateSpec {
	if in == nil {
		return nil
	}
	out := new(FirstMateSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FirstMateStatus) DeepCopyInto(out *FirstMateStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FirstMateStatus.
func (in *FirstMateStatus) DeepCopy() *FirstMateStatus {
	if in == nil {
		return nil
	}
	out := new(FirstMateStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4/cmd/main.go
================================================
/*
Copyright 2026 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 main

import (
	"crypto/tls"
	"flag"
	"os"

	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
	// to ensure that exec-entrypoint and run can make use of them.
	_ "k8s.io/client-go/plugin/pkg/client/auth"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
	crewv2 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v2"
	"sigs.k8s.io/kubebuilder/testdata/project-v4/internal/controller"
	webhookv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/internal/webhook/v1"
	// +kubebuilder:scaffold:imports
)

var (
	scheme   = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))

	utilruntime.Must(crewv1.AddToScheme(scheme))
	utilruntime.Must(crewv2.AddToScheme(scheme))
	utilruntime.Must(certmanagerv1.AddToScheme(scheme))
	// +kubebuilder:scaffold:scheme
}

// nolint:gocyclo
func main() {
	var metricsAddr string
	var metricsCertPath, metricsCertName, metricsCertKey string
	var webhookCertPath, webhookCertName, webhookCertKey string
	var enableLeaderElection bool
	var probeAddr string
	var secureMetrics bool
	var enableHTTP2 bool
	var tlsOpts []func(*tls.Config)
	flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
		"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
		"Enable leader election for controller manager. "+
			"Enabling this will ensure there is only one active controller manager.")
	flag.BoolVar(&secureMetrics, "metrics-secure", true,
		"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
	flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
	flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
	flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
	flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
		"The directory that contains the metrics server certificate.")
	flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
	flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
	flag.BoolVar(&enableHTTP2, "enable-http2", false,
		"If set, HTTP/2 will be enabled for the metrics and webhook servers")
	opts := zap.Options{
		Development: true,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	// if the enable-http2 flag is false (the default), http/2 should be disabled
	// due to its vulnerabilities. More specifically, disabling http/2 will
	// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
	// Rapid Reset CVEs. For more information see:
	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
	// - https://github.com/advisories/GHSA-4374-p667-p6c8
	disableHTTP2 := func(c *tls.Config) {
		setupLog.Info("Disabling HTTP/2")
		c.NextProtos = []string{"http/1.1"}
	}

	if !enableHTTP2 {
		tlsOpts = append(tlsOpts, disableHTTP2)
	}

	// Initial webhook TLS options
	webhookTLSOpts := tlsOpts
	webhookServerOptions := webhook.Options{
		TLSOpts: webhookTLSOpts,
	}

	if len(webhookCertPath) > 0 {
		setupLog.Info("Initializing webhook certificate watcher using provided certificates",
			"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)

		webhookServerOptions.CertDir = webhookCertPath
		webhookServerOptions.CertName = webhookCertName
		webhookServerOptions.KeyName = webhookCertKey
	}

	webhookServer := webhook.NewServer(webhookServerOptions)

	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
	// More info:
	// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/server
	// - https://book.kubebuilder.io/reference/metrics.html
	metricsServerOptions := metricsserver.Options{
		BindAddress:   metricsAddr,
		SecureServing: secureMetrics,
		TLSOpts:       tlsOpts,
	}

	if secureMetrics {
		// FilterProvider is used to protect the metrics endpoint with authn/authz.
		// These configurations ensure that only authorized users and service accounts
		// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
		// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/filters#WithAuthenticationAndAuthorization
		metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
	}

	// If the certificate is not specified, controller-runtime will automatically
	// generate self-signed certificates for the metrics server. While convenient for development and testing,
	// this setup is not recommended for production.
	//
	// TODO(user): If you enable certManager, uncomment the following lines:
	// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
	// managed by cert-manager for the metrics server.
	// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
	if len(metricsCertPath) > 0 {
		setupLog.Info("Initializing metrics certificate watcher using provided certificates",
			"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)

		metricsServerOptions.CertDir = metricsCertPath
		metricsServerOptions.CertName = metricsCertName
		metricsServerOptions.KeyName = metricsCertKey
	}

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:                 scheme,
		Metrics:                metricsServerOptions,
		WebhookServer:          webhookServer,
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       "da1d9c86.testproject.org",
		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	})
	if err != nil {
		setupLog.Error(err, "Failed to start manager")
		os.Exit(1)
	}

	if err := (&controller.CaptainReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Captain")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupCaptainWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Captain")
			os.Exit(1)
		}
	}
	if err := (&controller.FirstMateReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "FirstMate")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupFirstMateWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "FirstMate")
			os.Exit(1)
		}
	}
	if err := (&controller.SailorReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Sailor")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupSailorWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Sailor")
			os.Exit(1)
		}
	}
	if err := (&controller.AdmiralReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Admiral")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupAdmiralWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Admiral")
			os.Exit(1)
		}
	}
	if err := (&controller.CertificateReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Certificate")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupIssuerWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Issuer")
			os.Exit(1)
		}
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupPodWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Pod")
			os.Exit(1)
		}
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupDeploymentWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Deployment")
			os.Exit(1)
		}
	}
	// +kubebuilder:scaffold:builder

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up health check")
		os.Exit(1)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up ready check")
		os.Exit(1)
	}

	setupLog.Info("Starting manager")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "Failed to run manager")
		os.Exit(1)
	}
}


================================================
FILE: testdata/project-v4/config/certmanager/certificate-metrics.yaml
================================================
# The following manifests contain a self-signed issuer CR and a metrics certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: metrics-certs  # this name should match the one appeared in kustomizeconfig.yaml
  namespace: system
spec:
  dnsNames:
  # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
  # replacements in the config/default/kustomization.yaml file.
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: metrics-server-cert


================================================
FILE: testdata/project-v4/config/certmanager/certificate-webhook.yaml
================================================
# The following manifests contain a self-signed issuer CR and a certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: serving-cert  # this name should match the one appeared in kustomizeconfig.yaml
  namespace: system
spec:
  # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
  # replacements in the config/default/kustomization.yaml file.
  dnsNames:
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: webhook-server-cert


================================================
FILE: testdata/project-v4/config/certmanager/issuer.yaml
================================================
# The following manifest contains a self-signed issuer CR.
# More information can be found at https://docs.cert-manager.io
# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes.
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: selfsigned-issuer
  namespace: system
spec:
  selfSigned: {}


================================================
FILE: testdata/project-v4/config/certmanager/kustomization.yaml
================================================
resources:
- issuer.yaml
- certificate-webhook.yaml
- certificate-metrics.yaml

configurations:
- kustomizeconfig.yaml


================================================
FILE: testdata/project-v4/config/certmanager/kustomizeconfig.yaml
================================================
# This configuration is for teaching kustomize how to update name ref substitution
nameReference:
- kind: Issuer
  group: cert-manager.io
  fieldSpecs:
  - kind: Certificate
    group: cert-manager.io
    path: spec/issuerRef/name


================================================
FILE: testdata/project-v4/config/crd/bases/crew.testproject.org_admirales.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: admirales.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Admiral
    listKind: AdmiralList
    plural: admirales
    singular: admiral
  scope: Cluster
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Admiral is the Schema for the admirales API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Admiral
            properties:
              foo:
                description: foo is an example field of Admiral. Edit admiral_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Admiral
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Admiral resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4/config/crd/bases/crew.testproject.org_captains.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: captains.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Captain
    listKind: CaptainList
    plural: captains
    singular: captain
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Captain is the Schema for the captains API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Captain
            properties:
              foo:
                description: foo is an example field of Captain. Edit captain_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Captain
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Captain resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4/config/crd/bases/crew.testproject.org_firstmates.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: firstmates.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: FirstMate
    listKind: FirstMateList
    plural: firstmates
    singular: firstmate
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: FirstMate is the Schema for the firstmates API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of FirstMate
            properties:
              foo:
                description: foo is an example field of FirstMate. Edit firstmate_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of FirstMate
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the FirstMate resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
  - name: v2
    schema:
      openAPIV3Schema:
        description: FirstMate is the Schema for the firstmates API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of FirstMate
            properties:
              foo:
                description: foo is an example field of FirstMate. Edit firstmate_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of FirstMate
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the FirstMate resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: false
    subresources:
      status: {}


================================================
FILE: testdata/project-v4/config/crd/bases/crew.testproject.org_sailors.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: sailors.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Sailor
    listKind: SailorList
    plural: sailors
    singular: sailor
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Sailor is the Schema for the sailors API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Sailor
            properties:
              foo:
                description: foo is an example field of Sailor. Edit sailor_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Sailor
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Sailor resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4/config/crd/kustomization.yaml
================================================
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/crew.testproject.org_captains.yaml
- bases/crew.testproject.org_firstmates.yaml
- bases/crew.testproject.org_sailors.yaml
- bases/crew.testproject.org_admirales.yaml
# +kubebuilder:scaffold:crdkustomizeresource

patches:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
- path: patches/webhook_in_firstmates.yaml
# +kubebuilder:scaffold:crdkustomizewebhookpatch

# [WEBHOOK] To enable webhook, uncomment the following section
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml


================================================
FILE: testdata/project-v4/config/crd/kustomizeconfig.yaml
================================================
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
  version: v1
  fieldSpecs:
  - kind: CustomResourceDefinition
    version: v1
    group: apiextensions.k8s.io
    path: spec/conversion/webhook/clientConfig/service/name

varReference:
- path: metadata/annotations


================================================
FILE: testdata/project-v4/config/crd/patches/webhook_in_firstmates.yaml
================================================
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: firstmates.crew.testproject.org
spec:
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        service:
          namespace: system
          name: webhook-service
          path: /convert
      conversionReviewVersions:
      - v1


================================================
FILE: testdata/project-v4/config/default/cert_metrics_manager_patch.yaml
================================================
# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs.

# Add the volumeMount for the metrics-server certs
- op: add
  path: /spec/template/spec/containers/0/volumeMounts/-
  value:
    mountPath: /tmp/k8s-metrics-server/metrics-certs
    name: metrics-certs
    readOnly: true

# Add the --metrics-cert-path argument for the metrics server
- op: add
  path: /spec/template/spec/containers/0/args/-
  value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs

# Add the metrics-server certs volume configuration
- op: add
  path: /spec/template/spec/volumes/-
  value:
    name: metrics-certs
    secret:
      secretName: metrics-server-cert
      optional: false
      items:
        - key: ca.crt
          path: ca.crt
        - key: tls.crt
          path: tls.crt
        - key: tls.key
          path: tls.key


================================================
FILE: testdata/project-v4/config/default/kustomization.yaml
================================================
# Adds namespace to all resources.
namespace: project-v4-system

# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: project-v4-

# Labels to add to all resources and selectors.
#labels:
#- includeSelectors: true
#  pairs:
#    someName: someValue

resources:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
# [METRICS] Expose the controller manager metrics service.
- metrics_service.yaml
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
# be able to communicate with the Webhook Server.
#- ../network-policy

# Uncomment the patches line if you enable Metrics
patches:
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
# More info: https://book.kubebuilder.io/reference/metrics
- path: manager_metrics_patch.yaml
  target:
    kind: Deployment

# Uncomment the patches line if you enable Metrics and CertManager
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
# This patch will protect the metrics with certManager self-signed certs.
#- path: cert_metrics_manager_patch.yaml
#  target:
#    kind: Deployment

# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- path: manager_webhook_patch.yaml
  target:
    kind: Deployment

# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
replacements:
# - source: # Uncomment the following block to enable certificates for metrics
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.name
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 0
#         create: true
#     - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 0
#         create: true

# - source:
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.namespace
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 1
#         create: true
#     - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 1
#         create: true

 - source: # Uncomment the following block if you have any webhook
     kind: Service
     version: v1
     name: webhook-service
     fieldPath: .metadata.name # Name of the service
   targets:
     - select:
         kind: Certificate
         group: cert-manager.io
         version: v1
         name: serving-cert
       fieldPaths:
         - .spec.dnsNames.0
         - .spec.dnsNames.1
       options:
         delimiter: '.'
         index: 0
         create: true
 - source:
     kind: Service
     version: v1
     name: webhook-service
     fieldPath: .metadata.namespace # Namespace of the service
   targets:
     - select:
         kind: Certificate
         group: cert-manager.io
         version: v1
         name: serving-cert
       fieldPaths:
         - .spec.dnsNames.0
         - .spec.dnsNames.1
       options:
         delimiter: '.'
         index: 1
         create: true

 - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert # This name should match the one in certificate.yaml
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets:
     - select:
         kind: ValidatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets:
     - select:
         kind: ValidatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true

 - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets:
     - select:
         kind: MutatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets:
     - select:
         kind: MutatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true

 - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
     - select:
         kind: CustomResourceDefinition
         name: firstmates.crew.testproject.org
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
# +kubebuilder:scaffold:crdkustomizecainjectionns
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
     - select:
         kind: CustomResourceDefinition
         name: firstmates.crew.testproject.org
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true
# +kubebuilder:scaffold:crdkustomizecainjectionname


================================================
FILE: testdata/project-v4/config/default/manager_metrics_patch.yaml
================================================
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
- op: add
  path: /spec/template/spec/containers/0/args/0
  value: --metrics-bind-address=:8443


================================================
FILE: testdata/project-v4/config/default/manager_webhook_patch.yaml
================================================
# This patch ensures the webhook certificates are properly mounted in the manager container.
# It configures the necessary arguments, volumes, volume mounts, and container ports.

# Add the --webhook-cert-path argument for configuring the webhook certificate path
- op: add
  path: /spec/template/spec/containers/0/args/-
  value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs

# Add the volumeMount for the webhook certificates
- op: add
  path: /spec/template/spec/containers/0/volumeMounts/-
  value:
    mountPath: /tmp/k8s-webhook-server/serving-certs
    name: webhook-certs
    readOnly: true

# Add the port configuration for the webhook server
- op: add
  path: /spec/template/spec/containers/0/ports/-
  value:
    containerPort: 9443
    name: webhook-server
    protocol: TCP

# Add the volume configuration for the webhook certificates
- op: add
  path: /spec/template/spec/volumes/-
  value:
    name: webhook-certs
    secret:
      secretName: webhook-server-cert


================================================
FILE: testdata/project-v4/config/default/metrics_service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager-metrics-service
  namespace: system
spec:
  ports:
  - name: https
    port: 8443
    protocol: TCP
    targetPort: 8443
  selector:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4


================================================
FILE: testdata/project-v4/config/manager/kustomization.yaml
================================================
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
  newName: controller
  newTag: latest


================================================
FILE: testdata/project-v4/config/manager/manager.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: controller-manager
  namespace: system
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
spec:
  selector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4
  replicas: 1
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        control-plane: controller-manager
        app.kubernetes.io/name: project-v4
    spec:
      # TODO(user): Uncomment the following code to configure the nodeAffinity expression
      # according to the platforms which are supported by your solution.
      # It is considered best practice to support multiple architectures. You can
      # build your manager image using the makefile target docker-buildx.
      # affinity:
      #   nodeAffinity:
      #     requiredDuringSchedulingIgnoredDuringExecution:
      #       nodeSelectorTerms:
      #         - matchExpressions:
      #           - key: kubernetes.io/arch
      #             operator: In
      #             values:
      #               - amd64
      #               - arm64
      #               - ppc64le
      #               - s390x
      #           - key: kubernetes.io/os
      #             operator: In
      #             values:
      #               - linux
      securityContext:
        # Projects are configured by default to adhere to the "restricted" Pod Security Standards.
        # This ensures that deployments meet the highest security requirements for Kubernetes.
        # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
      - command:
        - /manager
        args:
          - --leader-elect
          - --health-probe-bind-address=:8081
        image: controller:latest
        name: manager
        ports: []
        securityContext:
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - "ALL"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 15
          periodSeconds: 20
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081
          initialDelaySeconds: 5
          periodSeconds: 10
        # TODO(user): Configure the resources accordingly based on the project requirements.
        # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        volumeMounts: []
      volumes: []
      serviceAccountName: controller-manager
      terminationGracePeriodSeconds: 10


================================================
FILE: testdata/project-v4/config/network-policy/allow-metrics-traffic.yaml
================================================
# This NetworkPolicy allows ingress traffic
# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
# namespaces are able to gather data from the metrics endpoint.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: allow-metrics-traffic
  namespace: system
spec:
  podSelector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4
  policyTypes:
    - Ingress
  ingress:
    # This allows ingress traffic from any namespace with the label metrics: enabled
    - from:
      - namespaceSelector:
          matchLabels:
            metrics: enabled  # Only from namespaces with this label
      ports:
        - port: 8443
          protocol: TCP


================================================
FILE: testdata/project-v4/config/network-policy/allow-webhook-traffic.yaml
================================================
# This NetworkPolicy allows ingress traffic to your webhook server running
# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks
# will only work when applied in namespaces labeled with 'webhook: enabled'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: allow-webhook-traffic
  namespace: system
spec:
  podSelector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4
  policyTypes:
    - Ingress
  ingress:
    # This allows ingress traffic from any namespace with the label webhook: enabled
    - from:
      - namespaceSelector:
          matchLabels:
            webhook: enabled # Only from namespaces with this label
      ports:
        - port: 443
          protocol: TCP


================================================
FILE: testdata/project-v4/config/network-policy/kustomization.yaml
================================================
resources:
- allow-webhook-traffic.yaml
- allow-metrics-traffic.yaml


================================================
FILE: testdata/project-v4/config/prometheus/kustomization.yaml
================================================
resources:
- monitor.yaml

# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
# to securely reference certificates created and managed by cert-manager.
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
# to mount the "metrics-server-cert" secret in the Manager Deployment.
#patches:
#  - path: monitor_tls_patch.yaml
#    target:
#      kind: ServiceMonitor


================================================
FILE: testdata/project-v4/config/prometheus/monitor.yaml
================================================
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager-metrics-monitor
  namespace: system
spec:
  endpoints:
    - path: /metrics
      port: https # Ensure this is the name of the port that exposes HTTPS metrics
      scheme: https
      bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
      tlsConfig:
        # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
        # certificate verification, exposing the system to potential man-in-the-middle attacks.
        # For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
        # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
        # which securely references the certificate from the 'metrics-server-cert' secret.
        insecureSkipVerify: true
  selector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4


================================================
FILE: testdata/project-v4/config/prometheus/monitor_tls_patch.yaml
================================================
# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
# using certificates managed by cert-manager
- op: replace
  path: /spec/endpoints/0/tlsConfig
  value:
    # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
    serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
    insecureSkipVerify: false
    ca:
      secret:
        name: metrics-server-cert
        key: ca.crt
    cert:
      secret:
        name: metrics-server-cert
        key: tls.crt
    keySecret:
      name: metrics-server-cert
      key: tls.key


================================================
FILE: testdata/project-v4/config/rbac/admiral_admin_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over crew.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: admiral-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/admiral_editor_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the crew.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: admiral-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/admiral_viewer_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to crew.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: admiral-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/captain_admin_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over crew.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: captain-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/captain_editor_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the crew.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: captain-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/captain_viewer_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to crew.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: captain-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/firstmate_admin_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over crew.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: firstmate-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/firstmate_editor_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the crew.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: firstmate-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/firstmate_viewer_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to crew.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: firstmate-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/kustomization.yaml
================================================
resources:
# All RBAC will be applied under this service account in
# the deployment namespace. You may comment out this resource
# if your manager will use a service account that exists at
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
# subjects if changing service account names.
- service_account.yaml
- role.yaml
- role_binding.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# The following RBAC configurations are used to protect
# the metrics endpoint with authn/authz. These configurations
# ensure that only authorized users and service accounts
# can access the metrics endpoint. Comment the following
# permissions if you want to disable this protection.
# More info: https://book.kubebuilder.io/reference/metrics.html
- metrics_auth_role.yaml
- metrics_auth_role_binding.yaml
- metrics_reader_role.yaml
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the project-v4 itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- admiral_admin_role.yaml
- admiral_editor_role.yaml
- admiral_viewer_role.yaml
- sailor_admin_role.yaml
- sailor_editor_role.yaml
- sailor_viewer_role.yaml
- firstmate_admin_role.yaml
- firstmate_editor_role.yaml
- firstmate_viewer_role.yaml
- captain_admin_role.yaml
- captain_editor_role.yaml
- captain_viewer_role.yaml



================================================
FILE: testdata/project-v4/config/rbac/leader_election_role.yaml
================================================
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: leader-election-role
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch


================================================
FILE: testdata/project-v4/config/rbac/leader_election_role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: leader-election-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: leader-election-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4/config/rbac/metrics_auth_role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: metrics-auth-role
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create


================================================
FILE: testdata/project-v4/config/rbac/metrics_auth_role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: metrics-auth-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: metrics-auth-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4/config/rbac/metrics_reader_role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: metrics-reader
rules:
- nonResourceURLs:
  - "/metrics"
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/role.yaml
================================================
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: manager-role
rules:
- apiGroups:
  - cert-manager.io
  resources:
  - certificates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/finalizers
  verbs:
  - update
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  - captains
  - firstmates
  - sailors
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/finalizers
  - captains/finalizers
  - firstmates/finalizers
  - sailors/finalizers
  verbs:
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  - captains/status
  - firstmates/status
  - sailors/status
  verbs:
  - get
  - patch
  - update


================================================
FILE: testdata/project-v4/config/rbac/role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: manager-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4/config/rbac/sailor_admin_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over crew.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: sailor-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/sailor_editor_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the crew.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: sailor-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/sailor_viewer_role.yaml
================================================
# This rule is not used by the project project-v4 itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to crew.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: sailor-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors/status
  verbs:
  - get


================================================
FILE: testdata/project-v4/config/rbac/service_account.yaml
================================================
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4/config/samples/crew_v1_admiral.yaml
================================================
apiVersion: crew.testproject.org/v1
kind: Admiral
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: admiral-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4/config/samples/crew_v1_captain.yaml
================================================
apiVersion: crew.testproject.org/v1
kind: Captain
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: captain-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4/config/samples/crew_v1_firstmate.yaml
================================================
apiVersion: crew.testproject.org/v1
kind: FirstMate
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: firstmate-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4/config/samples/crew_v1_sailor.yaml
================================================
apiVersion: crew.testproject.org/v1
kind: Sailor
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: sailor-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4/config/samples/crew_v2_firstmate.yaml
================================================
apiVersion: crew.testproject.org/v2
kind: FirstMate
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: firstmate-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4/config/samples/kustomization.yaml
================================================
## Append samples of your project ##
resources:
- crew_v1_captain.yaml
- crew_v1_firstmate.yaml
- crew_v2_firstmate.yaml
- crew_v1_sailor.yaml
- crew_v1_admiral.yaml
# +kubebuilder:scaffold:manifestskustomizesamples


================================================
FILE: testdata/project-v4/config/webhook/kustomization.yaml
================================================
resources:
- manifests.yaml
- service.yaml


================================================
FILE: testdata/project-v4/config/webhook/manifests.yaml
================================================
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-crew-testproject-org-v1-admiral
  failurePolicy: Fail
  name: madmiral-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - admirales
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: mcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-apps-v1-deployment
  failurePolicy: Fail
  name: mdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-cert-manager-io-v1-issuer
  failurePolicy: Fail
  name: missuer-v1.kb.io
  rules:
  - apiGroups:
    - cert-manager.io
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - issuers
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate--v1-pod
  failurePolicy: Fail
  name: mpod-v1.kb.io
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - pods
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /custom-mutate-sailor
  failurePolicy: Fail
  name: msailor-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - sailors
  sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /custom-validate-admiral
  failurePolicy: Fail
  name: vadmiral-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - admirales
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: vcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate-apps-v1-deployment
  failurePolicy: Fail
  name: vdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /custom-validate-sailor
  failurePolicy: Fail
  name: vsailor-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - sailors
  sideEffects: None


================================================
FILE: testdata/project-v4/config/webhook/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: project-v4
    app.kubernetes.io/managed-by: kustomize
  name: webhook-service
  namespace: system
spec:
  ports:
    - port: 443
      protocol: TCP
      targetPort: 9443
  selector:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4


================================================
FILE: testdata/project-v4/dist/install.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
    control-plane: controller-manager
  name: project-v4-system
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: admirales.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Admiral
    listKind: AdmiralList
    plural: admirales
    singular: admiral
  scope: Cluster
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Admiral is the Schema for the admirales API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Admiral
            properties:
              foo:
                description: foo is an example field of Admiral. Edit admiral_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Admiral
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Admiral resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: captains.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Captain
    listKind: CaptainList
    plural: captains
    singular: captain
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Captain is the Schema for the captains API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Captain
            properties:
              foo:
                description: foo is an example field of Captain. Edit captain_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Captain
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Captain resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-system/project-v4-serving-cert
    controller-gen.kubebuilder.io/version: v0.20.1
  name: firstmates.crew.testproject.org
spec:
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        service:
          name: project-v4-webhook-service
          namespace: project-v4-system
          path: /convert
      conversionReviewVersions:
      - v1
  group: crew.testproject.org
  names:
    kind: FirstMate
    listKind: FirstMateList
    plural: firstmates
    singular: firstmate
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: FirstMate is the Schema for the firstmates API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of FirstMate
            properties:
              foo:
                description: foo is an example field of FirstMate. Edit firstmate_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of FirstMate
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the FirstMate resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
  - name: v2
    schema:
      openAPIV3Schema:
        description: FirstMate is the Schema for the firstmates API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of FirstMate
            properties:
              foo:
                description: foo is an example field of FirstMate. Edit firstmate_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of FirstMate
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the FirstMate resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: false
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: sailors.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Sailor
    listKind: SailorList
    plural: sailors
    singular: sailor
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Sailor is the Schema for the sailors API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Sailor
            properties:
              foo:
                description: foo is an example field of Sailor. Edit sailor_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Sailor
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Sailor resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-controller-manager
  namespace: project-v4-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-leader-election-role
  namespace: project-v4-system
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-admiral-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-admiral-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-admiral-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-captain-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-captain-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-captain-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-firstmate-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-firstmate-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-firstmate-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - firstmates/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-manager-role
rules:
- apiGroups:
  - cert-manager.io
  resources:
  - certificates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/finalizers
  verbs:
  - update
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales
  - captains
  - firstmates
  - sailors
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/finalizers
  - captains/finalizers
  - firstmates/finalizers
  - sailors/finalizers
  verbs:
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - admirales/status
  - captains/status
  - firstmates/status
  - sailors/status
  verbs:
  - get
  - patch
  - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-metrics-auth-role
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-metrics-reader
rules:
- nonResourceURLs:
  - /metrics
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-sailor-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-sailor-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-sailor-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - sailors/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-leader-election-rolebinding
  namespace: project-v4-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: project-v4-leader-election-role
subjects:
- kind: ServiceAccount
  name: project-v4-controller-manager
  namespace: project-v4-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: project-v4-manager-role
subjects:
- kind: ServiceAccount
  name: project-v4-controller-manager
  namespace: project-v4-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: project-v4-metrics-auth-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: project-v4-metrics-auth-role
subjects:
- kind: ServiceAccount
  name: project-v4-controller-manager
  namespace: project-v4-system
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
    control-plane: controller-manager
  name: project-v4-controller-manager-metrics-service
  namespace: project-v4-system
spec:
  ports:
  - name: https
    port: 8443
    protocol: TCP
    targetPort: 8443
  selector:
    app.kubernetes.io/name: project-v4
    control-plane: controller-manager
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-webhook-service
  namespace: project-v4-system
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: 9443
  selector:
    app.kubernetes.io/name: project-v4
    control-plane: controller-manager
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
    control-plane: controller-manager
  name: project-v4-controller-manager
  namespace: project-v4-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: project-v4
      control-plane: controller-manager
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        app.kubernetes.io/name: project-v4
        control-plane: controller-manager
    spec:
      containers:
      - args:
        - --metrics-bind-address=:8443
        - --leader-elect
        - --health-probe-bind-address=:8081
        - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs
        command:
        - /manager
        image: controller:latest
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 15
          periodSeconds: 20
        name: manager
        ports:
        - containerPort: 9443
          name: webhook-server
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
          readOnlyRootFilesystem: true
        volumeMounts:
        - mountPath: /tmp/k8s-webhook-server/serving-certs
          name: webhook-certs
          readOnly: true
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      serviceAccountName: project-v4-controller-manager
      terminationGracePeriodSeconds: 10
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-server-cert
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-metrics-certs
  namespace: project-v4-system
spec:
  dnsNames:
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: project-v4-selfsigned-issuer
  secretName: metrics-server-cert
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-serving-cert
  namespace: project-v4-system
spec:
  dnsNames:
  - project-v4-webhook-service.project-v4-system.svc
  - project-v4-webhook-service.project-v4-system.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: project-v4-selfsigned-issuer
  secretName: webhook-server-cert
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4
  name: project-v4-selfsigned-issuer
  namespace: project-v4-system
spec:
  selfSigned: {}
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-system/project-v4-serving-cert
  name: project-v4-mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /mutate-crew-testproject-org-v1-admiral
  failurePolicy: Fail
  name: madmiral-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - admirales
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /mutate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: mcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /mutate-apps-v1-deployment
  failurePolicy: Fail
  name: mdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /mutate-cert-manager-io-v1-issuer
  failurePolicy: Fail
  name: missuer-v1.kb.io
  rules:
  - apiGroups:
    - cert-manager.io
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - issuers
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /mutate--v1-pod
  failurePolicy: Fail
  name: mpod-v1.kb.io
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - pods
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /custom-mutate-sailor
  failurePolicy: Fail
  name: msailor-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - sailors
  sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-system/project-v4-serving-cert
  name: project-v4-validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /custom-validate-admiral
  failurePolicy: Fail
  name: vadmiral-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - admirales
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /validate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: vcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /validate-apps-v1-deployment
  failurePolicy: Fail
  name: vdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-webhook-service
      namespace: project-v4-system
      path: /custom-validate-sailor
  failurePolicy: Fail
  name: vsailor-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - sailors
  sideEffects: None


================================================
FILE: testdata/project-v4/go.mod
================================================
module sigs.k8s.io/kubebuilder/testdata/project-v4

go 1.25.3

require (
	github.com/cert-manager/cert-manager v1.20.0
	github.com/onsi/ginkgo/v2 v2.28.0
	github.com/onsi/gomega v1.39.1
	k8s.io/api v0.35.2
	k8s.io/apimachinery v0.35.2
	k8s.io/client-go v0.35.2
	sigs.k8s.io/controller-runtime v0.23.3
)

require (
	cel.dev/expr v0.25.1 // indirect
	github.com/Masterminds/semver/v3 v3.4.0 // indirect
	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/blang/semver/v4 v4.0.0 // indirect
	github.com/cenkalti/backoff/v5 v5.0.3 // 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.13.0 // indirect
	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // 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-logr/zapr v1.3.0 // indirect
	github.com/go-openapi/jsonpointer v0.22.4 // indirect
	github.com/go-openapi/jsonreference v0.21.4 // indirect
	github.com/go-openapi/swag v0.23.1 // indirect
	github.com/go-openapi/swag/jsonname v0.25.4 // indirect
	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
	github.com/google/btree v1.1.3 // indirect
	github.com/google/cel-go v0.26.0 // indirect
	github.com/google/gnostic-models v0.7.1 // indirect
	github.com/google/go-cmp v0.7.0 // indirect
	github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // 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.9.1 // 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.17.0 // indirect
	github.com/spf13/cobra v1.10.2 // indirect
	github.com/spf13/pflag v1.0.10 // indirect
	github.com/stoewer/go-strcase v1.3.1 // indirect
	github.com/x448/float16 v0.8.4 // indirect
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
	go.opentelemetry.io/otel v1.40.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
	go.opentelemetry.io/otel/metric v1.40.0 // indirect
	go.opentelemetry.io/otel/sdk v1.40.0 // indirect
	go.opentelemetry.io/otel/trace v1.40.0 // indirect
	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
	go.uber.org/multierr v1.11.0 // indirect
	go.uber.org/zap v1.27.1 // 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-20250718183923-645b1fa84792 // indirect
	golang.org/x/mod v0.32.0 // indirect
	golang.org/x/net v0.51.0 // indirect
	golang.org/x/oauth2 v0.35.0 // indirect
	golang.org/x/sync v0.19.0 // indirect
	golang.org/x/sys v0.41.0 // indirect
	golang.org/x/term v0.40.0 // indirect
	golang.org/x/text v0.34.0 // indirect
	golang.org/x/time v0.14.0 // indirect
	golang.org/x/tools v0.41.0 // indirect
	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
	google.golang.org/grpc v1.79.1 // indirect
	google.golang.org/protobuf v1.36.11 // indirect
	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
	gopkg.in/inf.v0 v0.9.1 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
	k8s.io/apiextensions-apiserver v0.35.2 // indirect
	k8s.io/apiserver v0.35.2 // indirect
	k8s.io/component-base v0.35.2 // indirect
	k8s.io/klog/v2 v2.140.0 // indirect
	k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect
	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect
	sigs.k8s.io/gateway-api v1.5.0 // 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.2 // indirect
	sigs.k8s.io/yaml v1.6.0 // indirect
)


================================================
FILE: testdata/project-v4/hack/boilerplate.go.txt
================================================
/*
Copyright 2026 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: testdata/project-v4/internal/controller/admiral_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

// AdmiralReconciler reconciles a Admiral object
type AdmiralReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=crew.testproject.org,resources=admirales,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=crew.testproject.org,resources=admirales/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=crew.testproject.org,resources=admirales/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Admiral object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *AdmiralReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *AdmiralReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&crewv1.Admiral{}).
		Named("admiral").
		Complete(r)
}


================================================
FILE: testdata/project-v4/internal/controller/admiral_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

var _ = Describe("Admiral Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		admiral := &crewv1.Admiral{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Admiral")
			err := k8sClient.Get(ctx, typeNamespacedName, admiral)
			if err != nil && errors.IsNotFound(err) {
				resource := &crewv1.Admiral{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &crewv1.Admiral{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Admiral")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &AdmiralReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4/internal/controller/captain_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

// CaptainReconciler reconciles a Captain object
type CaptainReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=crew.testproject.org,resources=captains,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=crew.testproject.org,resources=captains/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=crew.testproject.org,resources=captains/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Captain object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *CaptainReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&crewv1.Captain{}).
		Named("captain").
		Complete(r)
}


================================================
FILE: testdata/project-v4/internal/controller/captain_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

var _ = Describe("Captain Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		captain := &crewv1.Captain{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Captain")
			err := k8sClient.Get(ctx, typeNamespacedName, captain)
			if err != nil && errors.IsNotFound(err) {
				resource := &crewv1.Captain{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &crewv1.Captain{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Captain")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &CaptainReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4/internal/controller/certificate_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// CertificateReconciler reconciles a Certificate object
type CertificateReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Certificate object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *CertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *CertificateReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&certmanagerv1.Certificate{}).
		Named("certificate").
		Complete(r)
}


================================================
FILE: testdata/project-v4/internal/controller/certificate_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	. "github.com/onsi/ginkgo/v2"
)

var _ = Describe("Certificate Controller", func() {
	Context("When reconciling a resource", func() {

		It("should successfully reconcile the resource", func() {

			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4/internal/controller/firstmate_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

// FirstMateReconciler reconciles a FirstMate object
type FirstMateReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=crew.testproject.org,resources=firstmates/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the FirstMate object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *FirstMateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *FirstMateReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&crewv1.FirstMate{}).
		Named("firstmate").
		Complete(r)
}


================================================
FILE: testdata/project-v4/internal/controller/firstmate_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

var _ = Describe("FirstMate Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		firstmate := &crewv1.FirstMate{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind FirstMate")
			err := k8sClient.Get(ctx, typeNamespacedName, firstmate)
			if err != nil && errors.IsNotFound(err) {
				resource := &crewv1.FirstMate{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &crewv1.FirstMate{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance FirstMate")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &FirstMateReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4/internal/controller/sailor_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

// SailorReconciler reconciles a Sailor object
type SailorReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=crew.testproject.org,resources=sailors,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=crew.testproject.org,resources=sailors/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=crew.testproject.org,resources=sailors/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Sailor object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *SailorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *SailorReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&crewv1.Sailor{}).
		Named("sailor").
		Complete(r)
}


================================================
FILE: testdata/project-v4/internal/controller/sailor_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

var _ = Describe("Sailor Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		sailor := &crewv1.Sailor{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Sailor")
			err := k8sClient.Get(ctx, typeNamespacedName, sailor)
			if err != nil && errors.IsNotFound(err) {
				resource := &crewv1.Sailor{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &crewv1.Sailor{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Sailor")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &SailorReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4/internal/controller/suite_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = crewv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	err = certmanagerv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4/internal/webhook/v1/admiral_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"

	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// nolint:unused
// log is for logging in this package.
var admirallog = logf.Log.WithName("admiral-resource")

// SetupAdmiralWebhookWithManager registers the webhook for Admiral in the manager.
func SetupAdmiralWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &crewv1.Admiral{}).
		WithDefaulter(&AdmiralCustomDefaulter{}).
		WithValidator(&AdmiralCustomValidator{}).
		WithValidatorCustomPath("/custom-validate-admiral").
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-crew-testproject-org-v1-admiral,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=admirales,verbs=create;update,versions=v1,name=madmiral-v1.kb.io,admissionReviewVersions=v1

// AdmiralCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Admiral when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type AdmiralCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Admiral.
func (d *AdmiralCustomDefaulter) Default(_ context.Context, obj *crewv1.Admiral) error {
	admirallog.Info("Defaulting for Admiral", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/custom-validate-admiral,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=admirales,verbs=create;update,versions=v1,name=vadmiral-v1.kb.io,admissionReviewVersions=v1

// AdmiralCustomValidator struct is responsible for validating the Admiral resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type AdmiralCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Admiral.
func (v *AdmiralCustomValidator) ValidateCreate(_ context.Context, obj *crewv1.Admiral) (admission.Warnings, error) {
	admirallog.Info("Validation for Admiral upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Admiral.
func (v *AdmiralCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *crewv1.Admiral) (admission.Warnings, error) {
	admirallog.Info("Validation for Admiral upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Admiral.
func (v *AdmiralCustomValidator) ValidateDelete(_ context.Context, obj *crewv1.Admiral) (admission.Warnings, error) {
	admirallog.Info("Validation for Admiral upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4/internal/webhook/v1/admiral_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Admiral Webhook", func() {
	var (
		obj       *crewv1.Admiral
		oldObj    *crewv1.Admiral
		defaulter AdmiralCustomDefaulter
		validator AdmiralCustomValidator
	)

	BeforeEach(func() {
		obj = &crewv1.Admiral{}
		oldObj = &crewv1.Admiral{}
		defaulter = AdmiralCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
		validator = AdmiralCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Admiral under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

	Context("When creating or updating Admiral under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4/internal/webhook/v1/captain_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"

	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// nolint:unused
// log is for logging in this package.
var captainlog = logf.Log.WithName("captain-resource")

// SetupCaptainWebhookWithManager registers the webhook for Captain in the manager.
func SetupCaptainWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &crewv1.Captain{}).
		WithDefaulter(&CaptainCustomDefaulter{}).
		WithValidator(&CaptainCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-crew-testproject-org-v1-captain,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=mcaptain-v1.kb.io,admissionReviewVersions=v1

// CaptainCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Captain when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type CaptainCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Captain.
func (d *CaptainCustomDefaulter) Default(_ context.Context, obj *crewv1.Captain) error {
	captainlog.Info("Defaulting for Captain", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-crew-testproject-org-v1-captain,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=vcaptain-v1.kb.io,admissionReviewVersions=v1

// CaptainCustomValidator struct is responsible for validating the Captain resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type CaptainCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Captain.
func (v *CaptainCustomValidator) ValidateCreate(_ context.Context, obj *crewv1.Captain) (admission.Warnings, error) {
	captainlog.Info("Validation for Captain upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Captain.
func (v *CaptainCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *crewv1.Captain) (admission.Warnings, error) {
	captainlog.Info("Validation for Captain upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Captain.
func (v *CaptainCustomValidator) ValidateDelete(_ context.Context, obj *crewv1.Captain) (admission.Warnings, error) {
	captainlog.Info("Validation for Captain upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4/internal/webhook/v1/captain_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Captain Webhook", func() {
	var (
		obj       *crewv1.Captain
		oldObj    *crewv1.Captain
		defaulter CaptainCustomDefaulter
		validator CaptainCustomValidator
	)

	BeforeEach(func() {
		obj = &crewv1.Captain{}
		oldObj = &crewv1.Captain{}
		defaulter = CaptainCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
		validator = CaptainCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Captain under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

	Context("When creating or updating Captain under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4/internal/webhook/v1/deployment_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	appsv1 "k8s.io/api/apps/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// nolint:unused
// log is for logging in this package.
var deploymentlog = logf.Log.WithName("deployment-resource")

// SetupDeploymentWebhookWithManager registers the webhook for Deployment in the manager.
func SetupDeploymentWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &appsv1.Deployment{}).
		WithDefaulter(&DeploymentCustomDefaulter{}).
		WithValidator(&DeploymentCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-apps-v1-deployment,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=mdeployment-v1.kb.io,admissionReviewVersions=v1

// DeploymentCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Deployment when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type DeploymentCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Deployment.
func (d *DeploymentCustomDefaulter) Default(_ context.Context, obj *appsv1.Deployment) error {
	deploymentlog.Info("Defaulting for Deployment", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-apps-v1-deployment,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=vdeployment-v1.kb.io,admissionReviewVersions=v1

// DeploymentCustomValidator struct is responsible for validating the Deployment resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type DeploymentCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Deployment.
func (v *DeploymentCustomValidator) ValidateCreate(_ context.Context, obj *appsv1.Deployment) (admission.Warnings, error) {
	deploymentlog.Info("Validation for Deployment upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Deployment.
func (v *DeploymentCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *appsv1.Deployment) (admission.Warnings, error) {
	deploymentlog.Info("Validation for Deployment upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Deployment.
func (v *DeploymentCustomValidator) ValidateDelete(_ context.Context, obj *appsv1.Deployment) (admission.Warnings, error) {
	deploymentlog.Info("Validation for Deployment upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4/internal/webhook/v1/deployment_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Deployment Webhook", func() {
	var (
		obj       *appsv1.Deployment
		oldObj    *appsv1.Deployment
		defaulter DeploymentCustomDefaulter
		validator DeploymentCustomValidator
	)

	BeforeEach(func() {
		obj = &appsv1.Deployment{}
		oldObj = &appsv1.Deployment{}
		defaulter = DeploymentCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
		validator = DeploymentCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Deployment under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

	Context("When creating or updating Deployment under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4/internal/webhook/v1/firstmate_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
)

// nolint:unused
// log is for logging in this package.
var firstmatelog = logf.Log.WithName("firstmate-resource")

// SetupFirstMateWebhookWithManager registers the webhook for FirstMate in the manager.
func SetupFirstMateWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &crewv1.FirstMate{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!


================================================
FILE: testdata/project-v4/internal/webhook/v1/firstmate_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("FirstMate Webhook", func() {
	var (
		obj    *crewv1.FirstMate
		oldObj *crewv1.FirstMate
	)

	BeforeEach(func() {
		obj = &crewv1.FirstMate{}
		oldObj = &crewv1.FirstMate{}
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating FirstMate under Conversion Webhook", func() {
		// TODO (user): Add logic to convert the object to the desired version and verify the conversion
		// Example:
		// It("Should convert the object correctly", func() {
		//     convertedObj := &crewv1.FirstMate{}
		//     Expect(obj.ConvertTo(convertedObj)).To(Succeed())
		//     Expect(convertedObj).ToNot(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4/internal/webhook/v1/issuer_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// nolint:unused
// log is for logging in this package.
var issuerlog = logf.Log.WithName("issuer-resource")

// SetupIssuerWebhookWithManager registers the webhook for Issuer in the manager.
func SetupIssuerWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &certmanagerv1.Issuer{}).
		WithDefaulter(&IssuerCustomDefaulter{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-cert-manager-io-v1-issuer,mutating=true,failurePolicy=fail,sideEffects=None,groups=cert-manager.io,resources=issuers,verbs=create;update,versions=v1,name=missuer-v1.kb.io,admissionReviewVersions=v1

// IssuerCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Issuer when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type IssuerCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Issuer.
func (d *IssuerCustomDefaulter) Default(_ context.Context, obj *certmanagerv1.Issuer) error {
	issuerlog.Info("Defaulting for Issuer", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}


================================================
FILE: testdata/project-v4/internal/webhook/v1/issuer_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Issuer Webhook", func() {
	var (
		obj       *certmanagerv1.Issuer
		oldObj    *certmanagerv1.Issuer
		defaulter IssuerCustomDefaulter
	)

	BeforeEach(func() {
		obj = &certmanagerv1.Issuer{}
		oldObj = &certmanagerv1.Issuer{}
		defaulter = IssuerCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Issuer under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

})


================================================
FILE: testdata/project-v4/internal/webhook/v1/pod_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	corev1 "k8s.io/api/core/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// nolint:unused
// log is for logging in this package.
var podlog = logf.Log.WithName("pod-resource")

// SetupPodWebhookWithManager registers the webhook for Pod in the manager.
func SetupPodWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &corev1.Pod{}).
		WithDefaulter(&PodCustomDefaulter{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,sideEffects=None,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod-v1.kb.io,admissionReviewVersions=v1

// PodCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Pod when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type PodCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Pod.
func (d *PodCustomDefaulter) Default(_ context.Context, obj *corev1.Pod) error {
	podlog.Info("Defaulting for Pod", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}


================================================
FILE: testdata/project-v4/internal/webhook/v1/pod_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	corev1 "k8s.io/api/core/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Pod Webhook", func() {
	var (
		obj       *corev1.Pod
		oldObj    *corev1.Pod
		defaulter PodCustomDefaulter
	)

	BeforeEach(func() {
		obj = &corev1.Pod{}
		oldObj = &corev1.Pod{}
		defaulter = PodCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Pod under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

})


================================================
FILE: testdata/project-v4/internal/webhook/v1/sailor_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"

	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// nolint:unused
// log is for logging in this package.
var sailorlog = logf.Log.WithName("sailor-resource")

// SetupSailorWebhookWithManager registers the webhook for Sailor in the manager.
func SetupSailorWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &crewv1.Sailor{}).
		WithDefaulter(&SailorCustomDefaulter{}).
		WithDefaulterCustomPath("/custom-mutate-sailor").
		WithValidator(&SailorCustomValidator{}).
		WithValidatorCustomPath("/custom-validate-sailor").
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/custom-mutate-sailor,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=sailors,verbs=create;update,versions=v1,name=msailor-v1.kb.io,admissionReviewVersions=v1

// SailorCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Sailor when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type SailorCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Sailor.
func (d *SailorCustomDefaulter) Default(_ context.Context, obj *crewv1.Sailor) error {
	sailorlog.Info("Defaulting for Sailor", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/custom-validate-sailor,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=sailors,verbs=create;update,versions=v1,name=vsailor-v1.kb.io,admissionReviewVersions=v1

// SailorCustomValidator struct is responsible for validating the Sailor resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type SailorCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Sailor.
func (v *SailorCustomValidator) ValidateCreate(_ context.Context, obj *crewv1.Sailor) (admission.Warnings, error) {
	sailorlog.Info("Validation for Sailor upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Sailor.
func (v *SailorCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *crewv1.Sailor) (admission.Warnings, error) {
	sailorlog.Info("Validation for Sailor upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Sailor.
func (v *SailorCustomValidator) ValidateDelete(_ context.Context, obj *crewv1.Sailor) (admission.Warnings, error) {
	sailorlog.Info("Validation for Sailor upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4/internal/webhook/v1/sailor_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Sailor Webhook", func() {
	var (
		obj       *crewv1.Sailor
		oldObj    *crewv1.Sailor
		defaulter SailorCustomDefaulter
		validator SailorCustomValidator
	)

	BeforeEach(func() {
		obj = &crewv1.Sailor{}
		oldObj = &crewv1.Sailor{}
		defaulter = SailorCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
		validator = SailorCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Sailor under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

	Context("When creating or updating Sailor under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4/internal/webhook/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = crewv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupCaptainWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	err = SetupFirstMateWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	err = SetupSailorWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	err = SetupAdmiralWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	err = SetupIssuerWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	err = SetupPodWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	err = SetupDeploymentWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4/test/e2e/e2e_suite_test.go
================================================
//go:build e2e
// +build e2e

/*
Copyright 2026 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 e2e

import (
	"fmt"
	"os"
	"os/exec"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/testdata/project-v4/test/utils"
)

var (
	// managerImage is the manager image to be built and loaded for testing.
	managerImage = "example.com/project-v4:v0.0.1"
	// shouldCleanupCertManager tracks whether CertManager was installed by this suite.
	shouldCleanupCertManager = false
)

// TestE2E runs the e2e test suite to validate the solution in an isolated environment.
// The default setup requires Kind and CertManager.
//
// To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true
func TestE2E(t *testing.T) {
	RegisterFailHandler(Fail)
	_, _ = fmt.Fprintf(GinkgoWriter, "Starting project-v4 e2e test suite\n")
	RunSpecs(t, "e2e suite")
}

var _ = BeforeSuite(func() {
	By("building the manager image")
	cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage))
	_, err := utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image")

	// TODO(user): If you want to change the e2e test vendor from Kind,
	// ensure the image is built and available, then remove the following block.
	By("loading the manager image on Kind")
	err = utils.LoadImageToKindClusterWithName(managerImage)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind")

	setupCertManager()
})

var _ = AfterSuite(func() {
	teardownCertManager()
})

// setupCertManager installs CertManager if needed for webhook tests.
// Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present.
func setupCertManager() {
	if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n")
		return
	}

	By("checking if CertManager is already installed")
	if utils.IsCertManagerCRDsInstalled() {
		_, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n")
		return
	}

	// Mark for cleanup before installation to handle interruptions and partial installs.
	shouldCleanupCertManager = true

	By("installing CertManager")
	Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
}

// teardownCertManager uninstalls CertManager if it was installed by setupCertManager.
// This ensures we only remove what we installed.
func teardownCertManager() {
	if !shouldCleanupCertManager {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n")
		return
	}

	By("uninstalling CertManager")
	utils.UninstallCertManager()
}


================================================
FILE: testdata/project-v4/test/e2e/e2e_test.go
================================================
//go:build e2e
// +build e2e

/*
Copyright 2026 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 e2e

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/testdata/project-v4/test/utils"
)

// namespace where the project is deployed in
const namespace = "project-v4-system"

// serviceAccountName created for the project
const serviceAccountName = "project-v4-controller-manager"

// metricsServiceName is the name of the metrics service of the project
const metricsServiceName = "project-v4-controller-manager-metrics-service"

// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
const metricsRoleBindingName = "project-v4-metrics-binding"

var _ = Describe("Manager", Ordered, func() {
	var controllerPodName string

	// Before running the tests, set up the environment by creating the namespace,
	// enforce the restricted security policy to the namespace, installing CRDs,
	// and deploying the controller.
	BeforeAll(func() {
		By("creating manager namespace")
		cmd := exec.Command("kubectl", "create", "ns", namespace)
		_, err := utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")

		By("labeling the namespace to enforce the restricted security policy")
		cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
			"pod-security.kubernetes.io/enforce=restricted")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")

		By("installing CRDs")
		cmd = exec.Command("make", "install")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")

		By("deploying the controller-manager")
		cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage))
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
	})

	// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
	// and deleting the namespace.
	AfterAll(func() {
		By("cleaning up the curl pod for metrics")
		cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
		_, _ = utils.Run(cmd)

		By("undeploying the controller-manager")
		cmd = exec.Command("make", "undeploy")
		_, _ = utils.Run(cmd)

		By("uninstalling CRDs")
		cmd = exec.Command("make", "uninstall")
		_, _ = utils.Run(cmd)

		By("removing manager namespace")
		cmd = exec.Command("kubectl", "delete", "ns", namespace)
		_, _ = utils.Run(cmd)
	})

	// After each test, check for failures and collect logs, events,
	// and pod descriptions for debugging.
	AfterEach(func() {
		specReport := CurrentSpecReport()
		if specReport.Failed() {
			By("Fetching controller manager pod logs")
			cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
			controllerLogs, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
			}

			By("Fetching Kubernetes events")
			cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
			eventsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
			}

			By("Fetching curl-metrics logs")
			cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
			metricsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
			}

			By("Fetching controller manager pod description")
			cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
			podDescription, err := utils.Run(cmd)
			if err == nil {
				fmt.Println("Pod description:\n", podDescription)
			} else {
				fmt.Println("Failed to describe controller pod")
			}
		}
	})

	SetDefaultEventuallyTimeout(2 * time.Minute)
	SetDefaultEventuallyPollingInterval(time.Second)

	Context("Manager", func() {
		It("should run successfully", func() {
			By("validating that the controller-manager pod is running as expected")
			verifyControllerUp := func(g Gomega) {
				// Get the name of the controller-manager pod
				cmd := exec.Command("kubectl", "get",
					"pods", "-l", "control-plane=controller-manager",
					"-o", "go-template={{ range .items }}"+
						"{{ if not .metadata.deletionTimestamp }}"+
						"{{ .metadata.name }}"+
						"{{ \"\\n\" }}{{ end }}{{ end }}",
					"-n", namespace,
				)

				podOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
				podNames := utils.GetNonEmptyLines(podOutput)
				g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
				controllerPodName = podNames[0]
				g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))

				// Validate the pod's status
				cmd = exec.Command("kubectl", "get",
					"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
					"-n", namespace,
				)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
			}
			Eventually(verifyControllerUp).Should(Succeed())
		})

		It("should ensure the metrics endpoint is serving metrics", func() {
			By("creating a ClusterRoleBinding for the service account to allow access to metrics")
			cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
				"--clusterrole=project-v4-metrics-reader",
				fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
			)
			_, err := utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")

			By("validating that the metrics service is available")
			cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")

			By("getting the service account token")
			token, err := serviceAccountToken()
			Expect(err).NotTo(HaveOccurred())
			Expect(token).NotTo(BeEmpty())

			By("ensuring the controller pod is ready")
			verifyControllerPodReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace,
					"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("True"), "Controller pod not ready")
			}
			Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying that the controller manager is serving the metrics server")
			verifyMetricsServerStarted := func(g Gomega) {
				cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(ContainSubstring("Serving metrics server"),
					"Metrics server not yet started")
			}
			Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed())

			By("waiting for the webhook service endpoints to be ready")
			verifyWebhookEndpointsReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "endpointslices.discovery.k8s.io", "-n", namespace,
					"-l", "kubernetes.io/service-name=project-v4-webhook-service",
					"-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist")
				g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready")
			}
			Eventually(verifyWebhookEndpointsReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying the mutating webhook server is ready")
			verifyMutatingWebhookReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "mutatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-mutating-webhook-configuration",
					"-o", "jsonpath={.webhooks[0].clientConfig.caBundle}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "MutatingWebhookConfiguration should exist")
				g.Expect(output).ShouldNot(BeEmpty(), "Mutating webhook CA bundle not yet injected")
			}
			Eventually(verifyMutatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed())

			By("waiting additional time for webhook server to stabilize")
			time.Sleep(5 * time.Second)

			// +kubebuilder:scaffold:e2e-metrics-webhooks-readiness

			By("creating the curl-metrics pod to access the metrics endpoint")
			cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
				"--namespace", namespace,
				"--image=curlimages/curl:latest",
				"--overrides",
				fmt.Sprintf(`{
					"spec": {
						"containers": [{
							"name": "curl",
							"image": "curlimages/curl:latest",
							"command": ["/bin/sh", "-c"],
							"args": [
								"for i in $(seq 1 30); do curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics && exit 0 || sleep 2; done; exit 1"
							],
							"securityContext": {
								"readOnlyRootFilesystem": true,
								"allowPrivilegeEscalation": false,
								"capabilities": {
									"drop": ["ALL"]
								},
								"runAsNonRoot": true,
								"runAsUser": 1000,
								"seccompProfile": {
									"type": "RuntimeDefault"
								}
							}
						}],
						"serviceAccountName": "%s"
					}
				}`, token, metricsServiceName, namespace, serviceAccountName))
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")

			By("waiting for the curl-metrics pod to complete.")
			verifyCurlUp := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
					"-o", "jsonpath={.status.phase}",
					"-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
			}
			Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())

			By("getting the metrics by checking curl-metrics logs")
			verifyMetricsAvailable := func(g Gomega) {
				metricsOutput, err := getMetricsOutput()
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
				g.Expect(metricsOutput).NotTo(BeEmpty())
				g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
			}
			Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
		})

		It("should provisioned cert-manager", func() {
			By("validating that cert-manager has the certificate Secret")
			verifyCertManager := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace)
				_, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
			}
			Eventually(verifyCertManager).Should(Succeed())
		})

		It("should have CA injection for mutating webhooks", func() {
			By("checking CA injection for mutating webhooks")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"mutatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-mutating-webhook-configuration",
					"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
				mwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(mwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		It("should have CA injection for validating webhooks", func() {
			By("checking CA injection for validating webhooks")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"validatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-validating-webhook-configuration",
					"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
				vwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		It("should have CA injection for FirstMate conversion webhook", func() {
			By("checking CA injection for FirstMate conversion webhook")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"customresourcedefinitions.apiextensions.k8s.io",
					"firstmates.crew.testproject.org",
					"-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}")
				vwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		// +kubebuilder:scaffold:e2e-webhooks-checks

		// TODO: Customize the e2e test suite with scenarios specific to your project.
		// Consider applying sample/CR(s) and check their status and/or verifying
		// the reconciliation by using the metrics, i.e.:
		// metricsOutput, err := getMetricsOutput()
		// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
		// Expect(metricsOutput).To(ContainSubstring(
		//    fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
		//    strings.ToLower(),
		// ))
	})
})

// serviceAccountToken returns a token for the specified service account in the given namespace.
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
// and parsing the resulting token from the API response.
func serviceAccountToken() (string, error) {
	const tokenRequestRawString = `{
		"apiVersion": "authentication.k8s.io/v1",
		"kind": "TokenRequest"
	}`

	// Temporary file to store the token request
	secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
	tokenRequestFile := filepath.Join("/tmp", secretName)
	err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
	if err != nil {
		return "", err
	}

	var out string
	verifyTokenCreation := func(g Gomega) {
		// Execute kubectl command to create the token
		cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
			"/api/v1/namespaces/%s/serviceaccounts/%s/token",
			namespace,
			serviceAccountName,
		), "-f", tokenRequestFile)

		output, err := cmd.CombinedOutput()
		g.Expect(err).NotTo(HaveOccurred())

		// Parse the JSON output to extract the token
		var token tokenRequest
		err = json.Unmarshal(output, &token)
		g.Expect(err).NotTo(HaveOccurred())

		out = token.Status.Token
	}
	Eventually(verifyTokenCreation).Should(Succeed())

	return out, err
}

// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
func getMetricsOutput() (string, error) {
	By("getting the curl-metrics logs")
	cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
	return utils.Run(cmd)
}

// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
// containing only the token field that we need to extract.
type tokenRequest struct {
	Status struct {
		Token string `json:"token"`
	} `json:"status"`
}


================================================
FILE: testdata/project-v4/test/utils/utils.go
================================================
/*
Copyright 2026 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 utils

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"

	. "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
)

const (
	certmanagerVersion = "v1.20.0"
	certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"

	defaultKindBinary  = "kind"
	defaultKindCluster = "kind"
)

func warnError(err error) {
	_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
}

// Run executes the provided command within this context
func Run(cmd *exec.Cmd) (string, error) {
	dir, _ := GetProjectDir()
	cmd.Dir = dir

	if err := os.Chdir(cmd.Dir); err != nil {
		_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
	}

	cmd.Env = append(os.Environ(), "GO111MODULE=on")
	command := strings.Join(cmd.Args, " ")
	_, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
	}

	return string(output), nil
}

// UninstallCertManager uninstalls the cert manager
func UninstallCertManager() {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "delete", "-f", url)
	if _, err := Run(cmd); err != nil {
		warnError(err)
	}

	// Delete leftover leases in kube-system (not cleaned by default)
	kubeSystemLeases := []string{
		"cert-manager-cainjector-leader-election",
		"cert-manager-controller",
	}
	for _, lease := range kubeSystemLeases {
		cmd = exec.Command("kubectl", "delete", "lease", lease,
			"-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
		if _, err := Run(cmd); err != nil {
			warnError(err)
		}
	}
}

// InstallCertManager installs the cert manager bundle.
func InstallCertManager() error {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "apply", "-f", url)
	if _, err := Run(cmd); err != nil {
		return err
	}
	// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
	// was re-installed after uninstalling on a cluster.
	cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
		"--for", "condition=Available",
		"--namespace", "cert-manager",
		"--timeout", "5m",
	)

	_, err := Run(cmd)
	return err
}

// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
// by verifying the existence of key CRDs related to Cert Manager.
func IsCertManagerCRDsInstalled() bool {
	// List of common Cert Manager CRDs
	certManagerCRDs := []string{
		"certificates.cert-manager.io",
		"issuers.cert-manager.io",
		"clusterissuers.cert-manager.io",
		"certificaterequests.cert-manager.io",
		"orders.acme.cert-manager.io",
		"challenges.acme.cert-manager.io",
	}

	// Execute the kubectl command to get all CRDs
	cmd := exec.Command("kubectl", "get", "crds")
	output, err := Run(cmd)
	if err != nil {
		return false
	}

	// Check if any of the Cert Manager CRDs are present
	crdList := GetNonEmptyLines(output)
	for _, crd := range certManagerCRDs {
		for _, line := range crdList {
			if strings.Contains(line, crd) {
				return true
			}
		}
	}

	return false
}

// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
func LoadImageToKindClusterWithName(name string) error {
	cluster := defaultKindCluster
	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
		cluster = v
	}
	kindOptions := []string{"load", "docker-image", name, "--name", cluster}
	kindBinary := defaultKindBinary
	if v, ok := os.LookupEnv("KIND"); ok {
		kindBinary = v
	}
	cmd := exec.Command(kindBinary, kindOptions...)
	_, err := Run(cmd)
	return err
}

// GetNonEmptyLines converts given command output string into individual objects
// according to line breakers, and ignores the empty elements in it.
func GetNonEmptyLines(output string) []string {
	var res []string
	elements := strings.SplitSeq(output, "\n")
	for element := range elements {
		if element != "" {
			res = append(res, element)
		}
	}

	return res
}

// GetProjectDir will return the directory where the project is
func GetProjectDir() (string, error) {
	wd, err := os.Getwd()
	if err != nil {
		return wd, fmt.Errorf("failed to get current working directory: %w", err)
	}
	wd = strings.ReplaceAll(wd, "/test/e2e", "")
	return wd, nil
}

// UncommentCode searches for target in the file and remove the comment prefix
// of the target content. The target content may span multiple lines.
func UncommentCode(filename, target, prefix string) error {
	// false positive
	// nolint:gosec
	content, err := os.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("failed to read file %q: %w", filename, err)
	}
	strContent := string(content)

	idx := strings.Index(strContent, target)
	if idx < 0 {
		return fmt.Errorf("unable to find the code %q to be uncommented", target)
	}

	out := new(bytes.Buffer)
	_, err = out.Write(content[:idx])
	if err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	scanner := bufio.NewScanner(bytes.NewBufferString(target))
	if !scanner.Scan() {
		return nil
	}
	for {
		if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
		// Avoid writing a newline in case the previous line was the last in target.
		if !scanner.Scan() {
			break
		}
		if _, err = out.WriteString("\n"); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
	}

	if _, err = out.Write(content[idx+len(target):]); err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	// false positive
	// nolint:gosec
	if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
		return fmt.Errorf("failed to write file %q: %w", filename, err)
	}

	return nil
}


================================================
FILE: testdata/project-v4-multigroup/.custom-gcl.yml
================================================
# This file configures golangci-lint with module plugins.
# When you run 'make lint', it will automatically build a custom golangci-lint binary
# with all the plugins listed below.
#
# See: https://golangci-lint.run/plugins/module-plugins/
version: v2.8.0
plugins:
  # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs)
  - module: "sigs.k8s.io/logtools"
    import: "sigs.k8s.io/logtools/logcheck/gclplugin"
    version: latest


================================================
FILE: testdata/project-v4-multigroup/.devcontainer/devcontainer.json
================================================
{
  "name": "Kubebuilder DevContainer",
  "image": "golang:1.25",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {
      "moby": false,
      "dockerDefaultAddressPool": "base=172.30.0.0/16,size=24"
    },
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/common-utils:2": {
      "upgradePackages": true
    }
  },

  "runArgs": ["--privileged", "--init"],

  "customizations": {
    "vscode": {
      "settings": {
        "terminal.integrated.shell.linux": "/bin/bash"
      },
      "extensions": [
        "ms-kubernetes-tools.vscode-kubernetes-tools",
        "ms-azuretools.vscode-docker"
      ]
    }
  },

  "remoteEnv": {
    "GO111MODULE": "on"
  },

  "onCreateCommand": "bash .devcontainer/post-install.sh"
}



================================================
FILE: testdata/project-v4-multigroup/.devcontainer/post-install.sh
================================================
#!/bin/bash
set -euo pipefail

echo "===================================="
echo "Kubebuilder DevContainer Setup"
echo "===================================="

# Verify running as root (required for installing to /usr/local/bin and /etc)
if [ "$(id -u)" -ne 0 ]; then
  echo "ERROR: This script must be run as root"
  exit 1
fi

echo ""
echo "Detecting system architecture..."
# Detect architecture using uname
MACHINE=$(uname -m)
case "${MACHINE}" in
  x86_64)
    ARCH="amd64"
    ;;
  aarch64|arm64)
    ARCH="arm64"
    ;;
  *)
    echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64"
    ARCH="amd64"
    ;;
esac
echo "Architecture: ${ARCH}"

echo ""
echo "------------------------------------"
echo "Setting up bash completion..."
echo "------------------------------------"

BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions"

# Enable bash-completion in root's .bashrc (devcontainer runs as root)
if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then
  echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc
  echo "Added bash-completion to .bashrc"
fi

echo ""
echo "------------------------------------"
echo "Installing development tools..."
echo "------------------------------------"

# Install kind
if ! command -v kind &> /dev/null; then
  echo "Installing kind..."
  curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}"
  chmod +x /usr/local/bin/kind
  echo "kind installed successfully"
fi

# Generate kind bash completion
if command -v kind &> /dev/null; then
  if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then
    echo "kind completion installed"
  else
    echo "WARNING: Failed to generate kind completion"
  fi
fi

# Install kubebuilder
if ! command -v kubebuilder &> /dev/null; then
  echo "Installing kubebuilder..."
  curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}"
  chmod +x /usr/local/bin/kubebuilder
  echo "kubebuilder installed successfully"
fi

# Generate kubebuilder bash completion
if command -v kubebuilder &> /dev/null; then
  if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then
    echo "kubebuilder completion installed"
  else
    echo "WARNING: Failed to generate kubebuilder completion"
  fi
fi

# Install kubectl
if ! command -v kubectl &> /dev/null; then
  echo "Installing kubectl..."
  KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt)
  curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl"
  chmod +x /usr/local/bin/kubectl
  echo "kubectl installed successfully"
fi

# Generate kubectl bash completion
if command -v kubectl &> /dev/null; then
  if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then
    echo "kubectl completion installed"
  else
    echo "WARNING: Failed to generate kubectl completion"
  fi
fi

# Generate Docker bash completion
if command -v docker &> /dev/null; then
  if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then
    echo "docker completion installed"
  else
    echo "WARNING: Failed to generate docker completion"
  fi
fi

echo ""
echo "------------------------------------"
echo "Configuring Docker environment..."
echo "------------------------------------"

# Wait for Docker to be ready
echo "Waiting for Docker to be ready..."
for i in {1..30}; do
  if docker info >/dev/null 2>&1; then
    echo "Docker is ready"
    break
  fi
  if [ "$i" -eq 30 ]; then
    echo "WARNING: Docker not ready after 30s"
  fi
  sleep 1
done

# Create kind network (ignore if already exists)
if ! docker network inspect kind >/dev/null 2>&1; then
  if docker network create kind >/dev/null 2>&1; then
    echo "Created kind network"
  else
    echo "WARNING: Failed to create kind network (may already exist)"
  fi
fi

echo ""
echo "------------------------------------"
echo "Verifying installations..."
echo "------------------------------------"
kind version
kubebuilder version
kubectl version --client
docker --version
go version

echo ""
echo "===================================="
echo "DevContainer ready!"
echo "===================================="
echo "All development tools installed successfully."
echo "You can now start building Kubernetes operators."


================================================
FILE: testdata/project-v4-multigroup/.dockerignore
================================================
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore everything by default and re-include only needed files
**

# Re-include Go source files (but not *_test.go)
!**/*.go
**/*_test.go

# Re-include Go module files
!go.mod
!go.sum


================================================
FILE: testdata/project-v4-multigroup/.github/workflows/lint.yml
================================================
name: Lint

on:
  push:
  pull_request:

jobs:
  lint:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Check linter configuration
        run: make lint-config
      - name: Run linter
        run: make lint


================================================
FILE: testdata/project-v4-multigroup/.github/workflows/test-e2e.yml
================================================
name: E2E Tests

on:
  push:
  pull_request:

jobs:
  test-e2e:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install the latest version of kind
        run: |
          curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind

      - name: Verify kind installation
        run: kind version

      - name: Running Test e2e
        run: |
          go mod tidy
          make test-e2e


================================================
FILE: testdata/project-v4-multigroup/.github/workflows/test.yml
================================================
name: Tests

on:
  push:
  pull_request:

jobs:
  test:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Running Tests
        run: |
          go mod tidy
          make test


================================================
FILE: testdata/project-v4-multigroup/.gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
Dockerfile.cross

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Go workspace file
go.work

# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*

# editor and IDE paraphernalia
.idea
.vscode
*.swp
*.swo
*~

# Kubeconfig might contain secrets
*.kubeconfig


================================================
FILE: testdata/project-v4-multigroup/.golangci.yml
================================================
version: "2"
run:
  allow-parallel-runners: true
linters:
  default: none
  enable:
    - copyloopvar
    - dupl
    - errcheck
    - ginkgolinter
    - goconst
    - gocyclo
    - govet
    - ineffassign
    - lll
    - modernize
    - misspell
    - nakedret
    - prealloc
    - revive
    - staticcheck
    - unconvert
    - unparam
    - unused
    - logcheck
  settings:
    custom:
      logcheck:
        type: "module"
        description: Checks Go logging calls for Kubernetes logging conventions.
    revive:
      rules:
        - name: comment-spacings
        - name: import-shadowing
    modernize:
      disable:
        - omitzero
  exclusions:
    generated: lax
    rules:
      - linters:
          - lll
        path: api/*
      - linters:
          - dupl
          - lll
        path: internal/*
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gofmt
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$


================================================
FILE: testdata/project-v4-multigroup/AGENTS.md
================================================
# project-v4-multigroup - AI Agent Guide

## Project Structure

**Single-group layout (default):**
```
cmd/main.go                    Manager entry (registers controllers/webhooks)
api//*_types.go       CRD schemas (+kubebuilder markers)
api//zz_generated.*   Auto-generated (DO NOT EDIT)
internal/controller/*          Reconciliation logic
internal/webhook/*             Validation/defaulting (if present)
config/crd/bases/*             Generated CRDs (DO NOT EDIT)
config/rbac/role.yaml          Generated RBAC (DO NOT EDIT)
config/samples/*               Example CRs (edit these)
Makefile                       Build/test/deploy commands
PROJECT                        Kubebuilder metadata Auto-generated (DO NOT EDIT)
```

**Multi-group layout** (for projects with multiple API groups):
```
api///*_types.go       CRD schemas by group
internal/controller//*          Controllers by group
internal/webhook///*   Webhooks by group and version (if present)
```

Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`.

**To convert to multi-group layout:**
1. Run: `kubebuilder edit --multigroup=true`
2. Move APIs: `mkdir -p api/ && mv api/ api//`
3. Move controllers: `mkdir -p internal/controller/ && mv internal/controller/*.go internal/controller//`
4. Move webhooks (if present): `mkdir -p internal/webhook/ && mv internal/webhook/ internal/webhook//`
5. Update import paths in all files
6. Fix `path` in `PROJECT` file for each resource
7. Update test suite CRD paths (add one more `..` to relative paths)

## Critical Rules

### Never Edit These (Auto-Generated)
- `config/crd/bases/*.yaml` - from `make manifests`
- `config/rbac/role.yaml` - from `make manifests`
- `config/webhook/manifests.yaml` - from `make manifests`
- `**/zz_generated.*.go` - from `make generate`
- `PROJECT` - from `kubebuilder [OPTIONS]`

### Never Remove Scaffold Markers
Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers.

### Keep Project Structure
Do not move files around. The CLI expects files in specific locations.

### Always Use CLI Commands
Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually.

### E2E Tests Require an Isolated Kind Cluster
The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI).
Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster).

## After Making Changes

**After editing `*_types.go` or markers:**
```
make manifests  # Regenerate CRDs/RBAC from markers
make generate   # Regenerate DeepCopy methods
```

**After editing `*.go` files:**
```
make lint-fix   # Auto-fix code style
make test       # Run unit tests
```

## CLI Commands Cheat Sheet

### Create API (your own types)
```bash
kubebuilder create api --group  --version  --kind 
```

### Deploy Image Plugin (scaffold to deploy/manage ANY container image)

Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.):

```bash
# Example: deploying memcached
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \
  --image=memcached:alpine \
  --plugins=deploy-image.go.kubebuilder.io/v1-alpha
```

Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation.


### Create Webhooks
```bash
# Validation + defaulting
kubebuilder create webhook --group  --version  --kind  \
  --defaulting --programmatic-validation

# Conversion webhook (for multi-version APIs)
kubebuilder create webhook --group  --version v1 --kind  \
  --conversion --spoke v2
```

### Controller for Core Kubernetes Types
```bash
# Watch Pods
kubebuilder create api --group core --version v1 --kind Pod \
  --controller=true --resource=false

# Watch Deployments
kubebuilder create api --group apps --version v1 --kind Deployment \
  --controller=true --resource=false
```

### Controller for External Types (e.g., from other operators)

Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.):

```bash
# Example: watching cert-manager Certificate resources
kubebuilder create api \
  --group cert-manager --version v1 --kind Certificate \
  --controller=true --resource=false \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
```

**Note:** Use `--external-api-module=@` only if you need a specific version. Otherwise, omit `@` to use what's in go.mod.

### Webhook for External Types

```bash
# Example: validating external resources
kubebuilder create webhook \
  --group cert-manager --version v1 --kind Issuer \
  --defaulting \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
```

## Testing & Development

```bash
make test              # Run unit tests (uses envtest: real K8s API + etcd)
make run               # Run locally (uses current kubeconfig context)
```

Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup.

## Deployment Workflow

```bash
# 1. Regenerate manifests
make manifests generate

# 2. Build & deploy
export IMG=/:tag
make docker-build docker-push IMG=$IMG  # Or: kind load docker-image $IMG --name 
make deploy IMG=$IMG

# 3. Test
kubectl apply -k config/samples/

# 4. Debug
kubectl logs -n -system deployment/-controller-manager -c manager -f
```

### API Design

**Key markers for** `api//*_types.go`:

```go
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"

// On fields:
// +kubebuilder:validation:Required
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:MaxLength=100
// +kubebuilder:validation:Pattern="^[a-z]+$"
// +kubebuilder:default="value"
```

- **Use** `metav1.Condition` for status (not custom string fields)
- **Use predefined types**: `metav1.Time` instead of `string` for dates
- **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`)

### Controller Design

**RBAC markers in** `internal/controller/*_controller.go`:

```go
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
```

**Implementation rules:**
- **Idempotent reconciliation**: Safe to run multiple times
- **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts
- **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)`
- **Owner references**: Enable automatic garbage collection (`SetControllerReference`)
- **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter`
- **Finalizers**: Clean up external resources (buckets, VMs, DNS entries)

### Logging

**Follow Kubernetes logging message style guidelines:**

- Start from a capital letter
- Do not end the message with a period
- Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`)
- Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"`
- Specify object type: `"Deleted Pod"` not `"Deleted"`
- Balanced key-value pairs

```go
log.Info("Starting reconciliation")
log.Info("Created Deployment", "name", deploy.Name)
log.Error(err, "Failed to create Pod", "name", name)
```

**Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines

### Webhooks
- **Create all types together**: `--defaulting --programmatic-validation --conversion`
- **When`--force`is used**: Backup custom logic first, then restore after scaffolding
- **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`)
  - Hub version: Usually oldest stable version (v1)
  - Spoke versions: Newer versions that convert to/from hub (v2, v3)
  - Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke)

### Learning from Examples

The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation:

```bash
kubebuilder create api --group example --version v1alpha1 --kind MyApp \
  --image= --plugins=deploy-image.go.kubebuilder.io/v1-alpha
```

Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation.

## Distribution Options

### Option 1: YAML Bundle (Kustomize)

```bash
# Generate dist/install.yaml from Kustomize manifests
make build-installer IMG=/:tag
```

**Key points:**
- The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment)
- Commit this file to your repository for easy distribution
- Users only need `kubectl` to install (no additional tools required)

**Example:** Users install with a single command:
```bash
kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml
```

### Option 2: Helm Chart

```bash
kubebuilder edit --plugins=helm/v2-alpha                      # Generates dist/chart/ (default)
kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts  # Generates charts/chart/
```

**For development:**
```bash
make helm-deploy IMG=/:          # Deploy manager via Helm
make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..."    # Deploy with custom values
make helm-status                                         # Show release status
make helm-uninstall                                      # Remove release
make helm-history                                        # View release history
make helm-rollback                                       # Rollback to previous version
```

**For end users/production:**
```bash
helm install my-release .//chart/ --namespace  --create-namespace
```

**Important:** If you add webhooks or modify manifests after initial chart generation:
1. Backup any customizations in `/chart/values.yaml` and `/chart/manager/manager.yaml`
2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized)
3. Manually restore your custom values from the backup

### Publish Container Image

```bash
export IMG=/:
make docker-build docker-push IMG=$IMG
```

## References

### Essential Reading
- **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide)
- **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions)
- **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.)
- **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels)

### API Design & Implementation
- **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- **Markers Reference**: https://book.kubebuilder.io/reference/markers.html

### Tools & Libraries
- **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime
- **controller-tools**: https://github.com/kubernetes-sigs/controller-tools
- **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder


================================================
FILE: testdata/project-v4-multigroup/Dockerfile
================================================
# Build the manager binary
FROM golang:1.25 AS builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

# Copy the Go source (relies on .dockerignore to filter)
COPY . .

# Build
# the GOARCH has no default value to allow the binary to be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]


================================================
FILE: testdata/project-v4-multigroup/Makefile
================================================
# Image URL to use all building/pushing image targets
IMG ?= controller:latest

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

# CONTAINER_TOOL defines the container tool to be used for building images.
# Be aware that the target commands are only tested with Docker which is
# scaffolded by default. However, you might want to replace it to use other
# tools. (i.e. podman)
CONTAINER_TOOL ?= docker

# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

.PHONY: all
all: build

##@ General

# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk command is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php

.PHONY: help
help: ## Display this help.
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Development

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
	"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."

.PHONY: fmt
fmt: ## Run go fmt against code.
	go fmt ./...

.PHONY: vet
vet: ## Run go vet against code.
	go vet ./...

.PHONY: test
test: manifests generate fmt vet setup-envtest ## Run tests.
	KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out

# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
# CertManager is installed by default; skip with:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= project-v4-multigroup-test-e2e

.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
	@command -v $(KIND) >/dev/null 2>&1 || { \
		echo "Kind is not installed. Please install Kind manually."; \
		exit 1; \
	}
	@case "$$($(KIND) get clusters)" in \
		*"$(KIND_CLUSTER)"*) \
			echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
		*) \
			echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
			$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
	esac

.PHONY: test-e2e
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
	KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
	$(MAKE) cleanup-test-e2e

.PHONY: cleanup-test-e2e
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
	@$(KIND) delete cluster --name $(KIND_CLUSTER)

.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
	"$(GOLANGCI_LINT)" run

.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
	"$(GOLANGCI_LINT)" run --fix

.PHONY: lint-config
lint-config: golangci-lint ## Verify golangci-lint linter configuration
	"$(GOLANGCI_LINT)" config verify

##@ Build

.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
	go build -o bin/manager cmd/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
	go run ./cmd/main.go

# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
	$(CONTAINER_TOOL) build -t ${IMG} .

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
	$(CONTAINER_TOOL) push ${IMG}

# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail)
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: ## Build and push docker image for the manager for cross-platform support
	# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
	sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
	- $(CONTAINER_TOOL) buildx create --name project-v4-multigroup-builder
	$(CONTAINER_TOOL) buildx use project-v4-multigroup-builder
	- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
	- $(CONTAINER_TOOL) buildx rm project-v4-multigroup-builder
	rm Dockerfile.cross

.PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
	mkdir -p dist
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default > dist/install.yaml

##@ Deployment

ifndef ignore-not-found
  ignore-not-found = false
endif

.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi

.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi

.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -

.PHONY: undeploy
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -

##@ Dependencies

## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
	mkdir -p "$(LOCALBIN)"

## Tool Binaries
KUBECTL ?= kubectl
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint

## Tool Versions
KUSTOMIZE_VERSION ?= v5.8.1
CONTROLLER_TOOLS_VERSION ?= v0.20.1

#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/')

#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')

GOLANGCI_LINT_VERSION ?= v2.8.0
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
	$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))

.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
	$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))

.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
	@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
	@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
		echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
		exit 1; \
	}

.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
	$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
	$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
	@test -f .custom-gcl.yml && { \
		echo "Building custom golangci-lint with plugins..." && \
		$(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \
		mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \
	} || true

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f "$(1)" ;\
GOBIN="$(LOCALBIN)" go install $${package} ;\
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
} ;\
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
endef

define gomodver
$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
endef


================================================
FILE: testdata/project-v4-multigroup/PROJECT
================================================
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
cliVersion: (devel)
domain: testproject.org
layout:
- go.kubebuilder.io/v4
multigroup: true
plugins:
  deploy-image.go.kubebuilder.io/v1-alpha:
    resources:
    - domain: testproject.org
      group: example.com
      kind: Memcached
      options:
        containerCommand: memcached,--memory-limit=64,-o,modern,-v
        containerPort: "11211"
        image: memcached:1.6.26-alpine3.19
        runAsUser: "1001"
      version: v1alpha1
    - domain: testproject.org
      group: example.com
      kind: Busybox
      options:
        image: busybox:1.36.1
      version: v1alpha1
  grafana.kubebuilder.io/v1-alpha: {}
projectName: project-v4-multigroup
repo: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup
resources:
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: crew
  kind: Captain
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1
  version: v1
  webhooks:
    defaulting: true
    validation: true
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: ship
  kind: Frigate
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1
  version: v1beta1
- api:
    crdVersion: v1
  controller: true
  domain: testproject.org
  group: ship
  kind: Destroyer
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1
  version: v1
  webhooks:
    defaulting: true
    webhookVersion: v1
- api:
    crdVersion: v1
  controller: true
  domain: testproject.org
  group: ship
  kind: Cruiser
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1
  version: v2alpha1
  webhooks:
    validation: true
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: sea-creatures
  kind: Kraken
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta1
  version: v1beta1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: sea-creatures
  kind: Leviathan
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta2
  version: v1beta2
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: foo.policy
  kind: HealthCheckPolicy
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo.policy/v1
  version: v1
- controller: true
  core: true
  group: apps
  kind: Deployment
  path: k8s.io/api/apps/v1
  version: v1
  webhooks:
    defaulting: true
    validation: true
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: foo
  kind: Bar
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo/v1
  version: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: fiz
  kind: Bar
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1
  version: v1
- controller: true
  domain: io
  external: true
  group: cert-manager
  kind: Certificate
  module: github.com/cert-manager/cert-manager@v1.20.0
  path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1
  version: v1
- domain: io
  external: true
  group: cert-manager
  kind: Issuer
  module: github.com/cert-manager/cert-manager@v1.20.0
  path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1
  version: v1
  webhooks:
    defaulting: true
    webhookVersion: v1
- core: true
  group: core
  kind: Pod
  path: k8s.io/api/core/v1
  version: v1
  webhooks:
    validation: true
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: example.com
  kind: Memcached
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1
  version: v1alpha1
  webhooks:
    validation: true
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: example.com
  kind: Busybox
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1
  version: v1alpha1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: example.com
  kind: Wordpress
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1
  version: v1
  webhooks:
    conversion: true
    spoke:
    - v2
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  domain: testproject.org
  group: example.com
  kind: Wordpress
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v2
  version: v2
version: "3"


================================================
FILE: testdata/project-v4-multigroup/README.md
================================================
# project-v4-multigroup
// TODO(user): Add simple overview of use/purpose

## Description
// TODO(user): An in-depth paragraph about your project and overview of use

## Getting Started

### Prerequisites
- go version v1.24.6+
- docker version 17.03+.
- kubectl version v1.11.3+.
- Access to a Kubernetes v1.11.3+ cluster.

### To Deploy on the cluster
**Build and push your image to the location specified by `IMG`:**

```sh
make docker-build docker-push IMG=/project-v4-multigroup:tag
```

**NOTE:** This image ought to be published in the personal registry you specified.
And it is required to have access to pull the image from the working environment.
Make sure you have the proper permission to the registry if the above commands don’t work.

**Install the CRDs into the cluster:**

```sh
make install
```

**Deploy the Manager to the cluster with the image specified by `IMG`:**

```sh
make deploy IMG=/project-v4-multigroup:tag
```

> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
privileges or be logged in as admin.

**Create instances of your solution**
You can apply the samples (examples) from the config/sample:

```sh
kubectl apply -k config/samples/
```

>**NOTE**: Ensure that the samples has default values to test it out.

### To Uninstall
**Delete the instances (CRs) from the cluster:**

```sh
kubectl delete -k config/samples/
```

**Delete the APIs(CRDs) from the cluster:**

```sh
make uninstall
```

**UnDeploy the controller from the cluster:**

```sh
make undeploy
```

## Project Distribution

Following the options to release and provide this solution to the users.

### By providing a bundle with all YAML files

1. Build the installer for the image built and published in the registry:

```sh
make build-installer IMG=/project-v4-multigroup:tag
```

**NOTE:** The makefile target mentioned above generates an 'install.yaml'
file in the dist directory. This file contains all the resources built
with Kustomize, which are necessary to install this project without its
dependencies.

2. Using the installer

Users can just run 'kubectl apply -f ' to install
the project, i.e.:

```sh
kubectl apply -f https://raw.githubusercontent.com//project-v4-multigroup//dist/install.yaml
```

### By providing a Helm Chart

1. Build the chart using the optional helm plugin

```sh
kubebuilder edit --plugins=helm/v2-alpha
```

2. See that a chart was generated under 'dist/chart', and users
can obtain this solution from there.

**NOTE:** If you change the project, you need to update the Helm Chart
using the same command above to sync the latest changes. Furthermore,
if you create webhooks, you need to use the above command with
the '--force' flag and manually ensure that any custom configuration
previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml'
is manually re-applied afterwards.

## Contributing
// TODO(user): Add detailed information on how you would like others to contribute to this project

**NOTE:** Run `make help` for more information on all potential `make` targets

More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)

## License

Copyright 2026 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: testdata/project-v4-multigroup/api/crew/v1/captain_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// CaptainSpec defines the desired state of Captain
type CaptainSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Captain. Edit captain_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// CaptainStatus defines the observed state of Captain.
type CaptainStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Captain resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Captain is the Schema for the captains API
type Captain struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Captain
	// +required
	Spec CaptainSpec `json:"spec"`

	// status defines the observed state of Captain
	// +optional
	Status CaptainStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// CaptainList contains a list of Captain
type CaptainList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Captain `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Captain{}, &CaptainList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/crew/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the crew v1 API group.
// +kubebuilder:object:generate=true
// +groupName=crew.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "crew.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/crew/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Captain) DeepCopyInto(out *Captain) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Captain.
func (in *Captain) DeepCopy() *Captain {
	if in == nil {
		return nil
	}
	out := new(Captain)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Captain) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CaptainList) DeepCopyInto(out *CaptainList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Captain, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptainList.
func (in *CaptainList) DeepCopy() *CaptainList {
	if in == nil {
		return nil
	}
	out := new(CaptainList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CaptainList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CaptainSpec) DeepCopyInto(out *CaptainSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptainSpec.
func (in *CaptainSpec) DeepCopy() *CaptainSpec {
	if in == nil {
		return nil
	}
	out := new(CaptainSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CaptainStatus) DeepCopyInto(out *CaptainStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CaptainStatus.
func (in *CaptainStatus) DeepCopy() *CaptainStatus {
	if in == nil {
		return nil
	}
	out := new(CaptainStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the example.com v1 API group.
// +kubebuilder:object:generate=true
// +groupName=example.com.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "example.com.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1/wordpress_conversion.go
================================================
/*
Copyright 2026 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 v1

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// Hub marks this type as a conversion hub.
func (*Wordpress) Hub() {}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1/wordpress_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// WordpressSpec defines the desired state of Wordpress
type WordpressSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Wordpress. Edit wordpress_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// WordpressStatus defines the observed state of Wordpress.
type WordpressStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Wordpress resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status

// Wordpress is the Schema for the wordpresses API
type Wordpress struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Wordpress
	// +required
	Spec WordpressSpec `json:"spec"`

	// status defines the observed state of Wordpress
	// +optional
	Status WordpressStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// WordpressList contains a list of Wordpress
type WordpressList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Wordpress `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Wordpress{}, &WordpressList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Wordpress) DeepCopyInto(out *Wordpress) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Wordpress.
func (in *Wordpress) DeepCopy() *Wordpress {
	if in == nil {
		return nil
	}
	out := new(Wordpress)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Wordpress) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressList) DeepCopyInto(out *WordpressList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Wordpress, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressList.
func (in *WordpressList) DeepCopy() *WordpressList {
	if in == nil {
		return nil
	}
	out := new(WordpressList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *WordpressList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressSpec) DeepCopyInto(out *WordpressSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressSpec.
func (in *WordpressSpec) DeepCopy() *WordpressSpec {
	if in == nil {
		return nil
	}
	out := new(WordpressSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressStatus) DeepCopyInto(out *WordpressStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressStatus.
func (in *WordpressStatus) DeepCopy() *WordpressStatus {
	if in == nil {
		return nil
	}
	out := new(WordpressStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1alpha1/busybox_types.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// BusyboxSpec defines the desired state of Busybox
type BusyboxSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// size defines the number of Busybox instances
	// +kubebuilder:default=1
	// +kubebuilder:validation:Minimum=0
	// +optional
	Size *int32 `json:"size,omitempty"`
}

// BusyboxStatus defines the observed state of Busybox
type BusyboxStatus struct {
	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Busybox resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Busybox is the Schema for the busyboxes API
type Busybox struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Busybox
	// +required
	Spec BusyboxSpec `json:"spec"`

	// status defines the observed state of Busybox
	// +optional
	Status BusyboxStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// BusyboxList contains a list of Busybox
type BusyboxList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Busybox `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Busybox{}, &BusyboxList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1alpha1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1alpha1 contains API Schema definitions for the example.com v1alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=example.com.testproject.org
package v1alpha1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "example.com.testproject.org", Version: "v1alpha1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1alpha1/memcached_types.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// MemcachedSpec defines the desired state of Memcached
type MemcachedSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// size defines the number of Memcached instances
	// +kubebuilder:default=1
	// +kubebuilder:validation:Minimum=0
	// +optional
	Size *int32 `json:"size,omitempty"`

	// containerPort defines the port that will be used to init the container with the image
	// +required
	ContainerPort int32 `json:"containerPort"`
}

// MemcachedStatus defines the observed state of Memcached
type MemcachedStatus struct {
	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Memcached resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Memcached is the Schema for the memcacheds API
type Memcached struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Memcached
	// +required
	Spec MemcachedSpec `json:"spec"`

	// status defines the observed state of Memcached
	// +optional
	Status MemcachedStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// MemcachedList contains a list of Memcached
type MemcachedList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Memcached `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Memcached{}, &MemcachedList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v1alpha1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1alpha1

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Busybox) DeepCopyInto(out *Busybox) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Busybox.
func (in *Busybox) DeepCopy() *Busybox {
	if in == nil {
		return nil
	}
	out := new(Busybox)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Busybox) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BusyboxList) DeepCopyInto(out *BusyboxList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Busybox, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxList.
func (in *BusyboxList) DeepCopy() *BusyboxList {
	if in == nil {
		return nil
	}
	out := new(BusyboxList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BusyboxList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BusyboxSpec) DeepCopyInto(out *BusyboxSpec) {
	*out = *in
	if in.Size != nil {
		in, out := &in.Size, &out.Size
		*out = new(int32)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxSpec.
func (in *BusyboxSpec) DeepCopy() *BusyboxSpec {
	if in == nil {
		return nil
	}
	out := new(BusyboxSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BusyboxStatus) DeepCopyInto(out *BusyboxStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxStatus.
func (in *BusyboxStatus) DeepCopy() *BusyboxStatus {
	if in == nil {
		return nil
	}
	out := new(BusyboxStatus)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Memcached) DeepCopyInto(out *Memcached) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Memcached.
func (in *Memcached) DeepCopy() *Memcached {
	if in == nil {
		return nil
	}
	out := new(Memcached)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Memcached) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MemcachedList) DeepCopyInto(out *MemcachedList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Memcached, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedList.
func (in *MemcachedList) DeepCopy() *MemcachedList {
	if in == nil {
		return nil
	}
	out := new(MemcachedList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MemcachedList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MemcachedSpec) DeepCopyInto(out *MemcachedSpec) {
	*out = *in
	if in.Size != nil {
		in, out := &in.Size, &out.Size
		*out = new(int32)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedSpec.
func (in *MemcachedSpec) DeepCopy() *MemcachedSpec {
	if in == nil {
		return nil
	}
	out := new(MemcachedSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MemcachedStatus) DeepCopyInto(out *MemcachedStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedStatus.
func (in *MemcachedStatus) DeepCopy() *MemcachedStatus {
	if in == nil {
		return nil
	}
	out := new(MemcachedStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v2/groupversion_info.go
================================================
/*
Copyright 2026 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 v2 contains API Schema definitions for the example.com v2 API group.
// +kubebuilder:object:generate=true
// +groupName=example.com.testproject.org
package v2

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "example.com.testproject.org", Version: "v2"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v2/wordpress_conversion.go
================================================
/*
Copyright 2026 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 v2

import (
	"log"

	"sigs.k8s.io/controller-runtime/pkg/conversion"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
)

// ConvertTo converts this Wordpress (v2) to the Hub version (v1).
func (src *Wordpress) ConvertTo(dstRaw conversion.Hub) error {
	dst := dstRaw.(*examplecomv1.Wordpress)
	log.Printf("ConvertTo: Converting Wordpress from Spoke version v2 to Hub version v1;"+
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)

	// TODO(user): Implement conversion logic from v2 to v1
	// Example: Copying Spec fields
	// dst.Spec.Size = src.Spec.Replicas

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}

// ConvertFrom converts the Hub version (v1) to this Wordpress (v2).
func (dst *Wordpress) ConvertFrom(srcRaw conversion.Hub) error {
	src := srcRaw.(*examplecomv1.Wordpress)
	log.Printf("ConvertFrom: Converting Wordpress from Hub version v1 to Spoke version v2;"+
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)

	// TODO(user): Implement conversion logic from v1 to v2
	// Example: Copying Spec fields
	// dst.Spec.Replicas = src.Spec.Size

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v2/wordpress_types.go
================================================
/*
Copyright 2026 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 v2

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// WordpressSpec defines the desired state of Wordpress
type WordpressSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Wordpress. Edit wordpress_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// WordpressStatus defines the observed state of Wordpress.
type WordpressStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Wordpress resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Wordpress is the Schema for the wordpresses API
type Wordpress struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Wordpress
	// +required
	Spec WordpressSpec `json:"spec"`

	// status defines the observed state of Wordpress
	// +optional
	Status WordpressStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// WordpressList contains a list of Wordpress
type WordpressList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Wordpress `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Wordpress{}, &WordpressList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/example.com/v2/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v2

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Wordpress) DeepCopyInto(out *Wordpress) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Wordpress.
func (in *Wordpress) DeepCopy() *Wordpress {
	if in == nil {
		return nil
	}
	out := new(Wordpress)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Wordpress) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressList) DeepCopyInto(out *WordpressList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Wordpress, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressList.
func (in *WordpressList) DeepCopy() *WordpressList {
	if in == nil {
		return nil
	}
	out := new(WordpressList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *WordpressList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressSpec) DeepCopyInto(out *WordpressSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressSpec.
func (in *WordpressSpec) DeepCopy() *WordpressSpec {
	if in == nil {
		return nil
	}
	out := new(WordpressSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressStatus) DeepCopyInto(out *WordpressStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressStatus.
func (in *WordpressStatus) DeepCopy() *WordpressStatus {
	if in == nil {
		return nil
	}
	out := new(WordpressStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/fiz/v1/bar_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// BarSpec defines the desired state of Bar
type BarSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Bar. Edit bar_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// BarStatus defines the observed state of Bar.
type BarStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Bar resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Bar is the Schema for the bars API
type Bar struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Bar
	// +required
	Spec BarSpec `json:"spec"`

	// status defines the observed state of Bar
	// +optional
	Status BarStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// BarList contains a list of Bar
type BarList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Bar `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Bar{}, &BarList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/fiz/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the fiz v1 API group.
// +kubebuilder:object:generate=true
// +groupName=fiz.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "fiz.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/fiz/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Bar) DeepCopyInto(out *Bar) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bar.
func (in *Bar) DeepCopy() *Bar {
	if in == nil {
		return nil
	}
	out := new(Bar)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Bar) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BarList) DeepCopyInto(out *BarList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Bar, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarList.
func (in *BarList) DeepCopy() *BarList {
	if in == nil {
		return nil
	}
	out := new(BarList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BarList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BarSpec) DeepCopyInto(out *BarSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarSpec.
func (in *BarSpec) DeepCopy() *BarSpec {
	if in == nil {
		return nil
	}
	out := new(BarSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BarStatus) DeepCopyInto(out *BarStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarStatus.
func (in *BarStatus) DeepCopy() *BarStatus {
	if in == nil {
		return nil
	}
	out := new(BarStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/foo/v1/bar_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// BarSpec defines the desired state of Bar
type BarSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Bar. Edit bar_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// BarStatus defines the observed state of Bar.
type BarStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Bar resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Bar is the Schema for the bars API
type Bar struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Bar
	// +required
	Spec BarSpec `json:"spec"`

	// status defines the observed state of Bar
	// +optional
	Status BarStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// BarList contains a list of Bar
type BarList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Bar `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Bar{}, &BarList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/foo/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the foo v1 API group.
// +kubebuilder:object:generate=true
// +groupName=foo.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "foo.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/foo/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Bar) DeepCopyInto(out *Bar) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bar.
func (in *Bar) DeepCopy() *Bar {
	if in == nil {
		return nil
	}
	out := new(Bar)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Bar) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BarList) DeepCopyInto(out *BarList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Bar, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarList.
func (in *BarList) DeepCopy() *BarList {
	if in == nil {
		return nil
	}
	out := new(BarList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BarList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BarSpec) DeepCopyInto(out *BarSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarSpec.
func (in *BarSpec) DeepCopy() *BarSpec {
	if in == nil {
		return nil
	}
	out := new(BarSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BarStatus) DeepCopyInto(out *BarStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BarStatus.
func (in *BarStatus) DeepCopy() *BarStatus {
	if in == nil {
		return nil
	}
	out := new(BarStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/foo.policy/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the foo.policy v1 API group.
// +kubebuilder:object:generate=true
// +groupName=foo.policy.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "foo.policy.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/foo.policy/v1/healthcheckpolicy_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// HealthCheckPolicySpec defines the desired state of HealthCheckPolicy
type HealthCheckPolicySpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of HealthCheckPolicy. Edit healthcheckpolicy_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// HealthCheckPolicyStatus defines the observed state of HealthCheckPolicy.
type HealthCheckPolicyStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the HealthCheckPolicy resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// HealthCheckPolicy is the Schema for the healthcheckpolicies API
type HealthCheckPolicy struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of HealthCheckPolicy
	// +required
	Spec HealthCheckPolicySpec `json:"spec"`

	// status defines the observed state of HealthCheckPolicy
	// +optional
	Status HealthCheckPolicyStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// HealthCheckPolicyList contains a list of HealthCheckPolicy
type HealthCheckPolicyList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []HealthCheckPolicy `json:"items"`
}

func init() {
	SchemeBuilder.Register(&HealthCheckPolicy{}, &HealthCheckPolicyList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/foo.policy/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthCheckPolicy) DeepCopyInto(out *HealthCheckPolicy) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckPolicy.
func (in *HealthCheckPolicy) DeepCopy() *HealthCheckPolicy {
	if in == nil {
		return nil
	}
	out := new(HealthCheckPolicy)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *HealthCheckPolicy) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthCheckPolicyList) DeepCopyInto(out *HealthCheckPolicyList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]HealthCheckPolicy, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckPolicyList.
func (in *HealthCheckPolicyList) DeepCopy() *HealthCheckPolicyList {
	if in == nil {
		return nil
	}
	out := new(HealthCheckPolicyList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *HealthCheckPolicyList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthCheckPolicySpec) DeepCopyInto(out *HealthCheckPolicySpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckPolicySpec.
func (in *HealthCheckPolicySpec) DeepCopy() *HealthCheckPolicySpec {
	if in == nil {
		return nil
	}
	out := new(HealthCheckPolicySpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *HealthCheckPolicyStatus) DeepCopyInto(out *HealthCheckPolicyStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheckPolicyStatus.
func (in *HealthCheckPolicyStatus) DeepCopy() *HealthCheckPolicyStatus {
	if in == nil {
		return nil
	}
	out := new(HealthCheckPolicyStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/sea-creatures/v1beta1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1beta1 contains API Schema definitions for the sea-creatures v1beta1 API group.
// +kubebuilder:object:generate=true
// +groupName=sea-creatures.testproject.org
package v1beta1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "sea-creatures.testproject.org", Version: "v1beta1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/sea-creatures/v1beta1/kraken_types.go
================================================
/*
Copyright 2026 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 v1beta1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// KrakenSpec defines the desired state of Kraken
type KrakenSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Kraken. Edit kraken_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// KrakenStatus defines the observed state of Kraken.
type KrakenStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Kraken resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Kraken is the Schema for the krakens API
type Kraken struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Kraken
	// +required
	Spec KrakenSpec `json:"spec"`

	// status defines the observed state of Kraken
	// +optional
	Status KrakenStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// KrakenList contains a list of Kraken
type KrakenList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Kraken `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Kraken{}, &KrakenList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/sea-creatures/v1beta1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1beta1

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Kraken) DeepCopyInto(out *Kraken) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kraken.
func (in *Kraken) DeepCopy() *Kraken {
	if in == nil {
		return nil
	}
	out := new(Kraken)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Kraken) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KrakenList) DeepCopyInto(out *KrakenList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Kraken, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KrakenList.
func (in *KrakenList) DeepCopy() *KrakenList {
	if in == nil {
		return nil
	}
	out := new(KrakenList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *KrakenList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KrakenSpec) DeepCopyInto(out *KrakenSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KrakenSpec.
func (in *KrakenSpec) DeepCopy() *KrakenSpec {
	if in == nil {
		return nil
	}
	out := new(KrakenSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *KrakenStatus) DeepCopyInto(out *KrakenStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KrakenStatus.
func (in *KrakenStatus) DeepCopy() *KrakenStatus {
	if in == nil {
		return nil
	}
	out := new(KrakenStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/sea-creatures/v1beta2/groupversion_info.go
================================================
/*
Copyright 2026 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 v1beta2 contains API Schema definitions for the sea-creatures v1beta2 API group.
// +kubebuilder:object:generate=true
// +groupName=sea-creatures.testproject.org
package v1beta2

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "sea-creatures.testproject.org", Version: "v1beta2"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/sea-creatures/v1beta2/leviathan_types.go
================================================
/*
Copyright 2026 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 v1beta2

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// LeviathanSpec defines the desired state of Leviathan
type LeviathanSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Leviathan. Edit leviathan_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// LeviathanStatus defines the observed state of Leviathan.
type LeviathanStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Leviathan resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Leviathan is the Schema for the leviathans API
type Leviathan struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Leviathan
	// +required
	Spec LeviathanSpec `json:"spec"`

	// status defines the observed state of Leviathan
	// +optional
	Status LeviathanStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// LeviathanList contains a list of Leviathan
type LeviathanList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Leviathan `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Leviathan{}, &LeviathanList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/sea-creatures/v1beta2/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1beta2

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Leviathan) DeepCopyInto(out *Leviathan) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Leviathan.
func (in *Leviathan) DeepCopy() *Leviathan {
	if in == nil {
		return nil
	}
	out := new(Leviathan)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Leviathan) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LeviathanList) DeepCopyInto(out *LeviathanList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Leviathan, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeviathanList.
func (in *LeviathanList) DeepCopy() *LeviathanList {
	if in == nil {
		return nil
	}
	out := new(LeviathanList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *LeviathanList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LeviathanSpec) DeepCopyInto(out *LeviathanSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeviathanSpec.
func (in *LeviathanSpec) DeepCopy() *LeviathanSpec {
	if in == nil {
		return nil
	}
	out := new(LeviathanSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *LeviathanStatus) DeepCopyInto(out *LeviathanStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LeviathanStatus.
func (in *LeviathanStatus) DeepCopy() *LeviathanStatus {
	if in == nil {
		return nil
	}
	out := new(LeviathanStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/ship/v1/destroyer_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// DestroyerSpec defines the desired state of Destroyer
type DestroyerSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Destroyer. Edit destroyer_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// DestroyerStatus defines the observed state of Destroyer.
type DestroyerStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Destroyer resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster

// Destroyer is the Schema for the destroyers API
type Destroyer struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Destroyer
	// +required
	Spec DestroyerSpec `json:"spec"`

	// status defines the observed state of Destroyer
	// +optional
	Status DestroyerStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// DestroyerList contains a list of Destroyer
type DestroyerList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Destroyer `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Destroyer{}, &DestroyerList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/ship/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the ship v1 API group.
// +kubebuilder:object:generate=true
// +groupName=ship.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "ship.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/ship/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Destroyer) DeepCopyInto(out *Destroyer) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Destroyer.
func (in *Destroyer) DeepCopy() *Destroyer {
	if in == nil {
		return nil
	}
	out := new(Destroyer)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Destroyer) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DestroyerList) DeepCopyInto(out *DestroyerList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Destroyer, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DestroyerList.
func (in *DestroyerList) DeepCopy() *DestroyerList {
	if in == nil {
		return nil
	}
	out := new(DestroyerList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *DestroyerList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DestroyerSpec) DeepCopyInto(out *DestroyerSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DestroyerSpec.
func (in *DestroyerSpec) DeepCopy() *DestroyerSpec {
	if in == nil {
		return nil
	}
	out := new(DestroyerSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DestroyerStatus) DeepCopyInto(out *DestroyerStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DestroyerStatus.
func (in *DestroyerStatus) DeepCopy() *DestroyerStatus {
	if in == nil {
		return nil
	}
	out := new(DestroyerStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/ship/v1beta1/frigate_types.go
================================================
/*
Copyright 2026 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 v1beta1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// FrigateSpec defines the desired state of Frigate
type FrigateSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Frigate. Edit frigate_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// FrigateStatus defines the observed state of Frigate.
type FrigateStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Frigate resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Frigate is the Schema for the frigates API
type Frigate struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Frigate
	// +required
	Spec FrigateSpec `json:"spec"`

	// status defines the observed state of Frigate
	// +optional
	Status FrigateStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// FrigateList contains a list of Frigate
type FrigateList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Frigate `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Frigate{}, &FrigateList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/ship/v1beta1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1beta1 contains API Schema definitions for the ship v1beta1 API group.
// +kubebuilder:object:generate=true
// +groupName=ship.testproject.org
package v1beta1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "ship.testproject.org", Version: "v1beta1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/ship/v1beta1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1beta1

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Frigate) DeepCopyInto(out *Frigate) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Frigate.
func (in *Frigate) DeepCopy() *Frigate {
	if in == nil {
		return nil
	}
	out := new(Frigate)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Frigate) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FrigateList) DeepCopyInto(out *FrigateList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Frigate, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrigateList.
func (in *FrigateList) DeepCopy() *FrigateList {
	if in == nil {
		return nil
	}
	out := new(FrigateList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *FrigateList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FrigateSpec) DeepCopyInto(out *FrigateSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrigateSpec.
func (in *FrigateSpec) DeepCopy() *FrigateSpec {
	if in == nil {
		return nil
	}
	out := new(FrigateSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *FrigateStatus) DeepCopyInto(out *FrigateStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FrigateStatus.
func (in *FrigateStatus) DeepCopy() *FrigateStatus {
	if in == nil {
		return nil
	}
	out := new(FrigateStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/api/ship/v2alpha1/cruiser_types.go
================================================
/*
Copyright 2026 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 v2alpha1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// CruiserSpec defines the desired state of Cruiser
type CruiserSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Cruiser. Edit cruiser_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// CruiserStatus defines the observed state of Cruiser.
type CruiserStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Cruiser resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster

// Cruiser is the Schema for the cruisers API
type Cruiser struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Cruiser
	// +required
	Spec CruiserSpec `json:"spec"`

	// status defines the observed state of Cruiser
	// +optional
	Status CruiserStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// CruiserList contains a list of Cruiser
type CruiserList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Cruiser `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Cruiser{}, &CruiserList{})
}


================================================
FILE: testdata/project-v4-multigroup/api/ship/v2alpha1/groupversion_info.go
================================================
/*
Copyright 2026 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 v2alpha1 contains API Schema definitions for the ship v2alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=ship.testproject.org
package v2alpha1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "ship.testproject.org", Version: "v2alpha1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-multigroup/api/ship/v2alpha1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v2alpha1

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Cruiser) DeepCopyInto(out *Cruiser) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cruiser.
func (in *Cruiser) DeepCopy() *Cruiser {
	if in == nil {
		return nil
	}
	out := new(Cruiser)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Cruiser) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CruiserList) DeepCopyInto(out *CruiserList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Cruiser, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CruiserList.
func (in *CruiserList) DeepCopy() *CruiserList {
	if in == nil {
		return nil
	}
	out := new(CruiserList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *CruiserList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CruiserSpec) DeepCopyInto(out *CruiserSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CruiserSpec.
func (in *CruiserSpec) DeepCopy() *CruiserSpec {
	if in == nil {
		return nil
	}
	out := new(CruiserSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *CruiserStatus) DeepCopyInto(out *CruiserStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CruiserStatus.
func (in *CruiserStatus) DeepCopy() *CruiserStatus {
	if in == nil {
		return nil
	}
	out := new(CruiserStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-multigroup/cmd/main.go
================================================
/*
Copyright 2026 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 main

import (
	"crypto/tls"
	"flag"
	"os"

	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
	// to ensure that exec-entrypoint and run can make use of them.
	_ "k8s.io/client-go/plugin/pkg/client/auth"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1"
	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
	examplecomv2 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v2"
	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1"
	foopolicyv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo.policy/v1"
	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo/v1"
	seacreaturesv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta1"
	seacreaturesv1beta2 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta2"
	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1"
	shipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1"
	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1"
	appscontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/apps"
	certmanagercontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/cert-manager"
	crewcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/crew"
	examplecomcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/example.com"
	fizcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/fiz"
	foocontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/foo"
	foopolicycontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/foo.policy"
	seacreaturescontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/sea-creatures"
	shipcontroller "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/controller/ship"
	webhookappsv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/apps/v1"
	webhookcertmanagerv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/cert-manager/v1"
	webhookcorev1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/core/v1"
	webhookcrewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/crew/v1"
	webhookexamplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/example.com/v1"
	webhookexamplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1"
	webhookshipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/ship/v1"
	webhookshipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1"
	// +kubebuilder:scaffold:imports
)

var (
	scheme   = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))

	utilruntime.Must(crewv1.AddToScheme(scheme))
	utilruntime.Must(shipv1beta1.AddToScheme(scheme))
	utilruntime.Must(shipv1.AddToScheme(scheme))
	utilruntime.Must(shipv2alpha1.AddToScheme(scheme))
	utilruntime.Must(seacreaturesv1beta1.AddToScheme(scheme))
	utilruntime.Must(seacreaturesv1beta2.AddToScheme(scheme))
	utilruntime.Must(foopolicyv1.AddToScheme(scheme))
	utilruntime.Must(foov1.AddToScheme(scheme))
	utilruntime.Must(fizv1.AddToScheme(scheme))
	utilruntime.Must(certmanagerv1.AddToScheme(scheme))
	utilruntime.Must(examplecomv1alpha1.AddToScheme(scheme))
	utilruntime.Must(examplecomv1.AddToScheme(scheme))
	utilruntime.Must(examplecomv2.AddToScheme(scheme))
	// +kubebuilder:scaffold:scheme
}

// nolint:gocyclo
func main() {
	var metricsAddr string
	var metricsCertPath, metricsCertName, metricsCertKey string
	var webhookCertPath, webhookCertName, webhookCertKey string
	var enableLeaderElection bool
	var probeAddr string
	var secureMetrics bool
	var enableHTTP2 bool
	var tlsOpts []func(*tls.Config)
	flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
		"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
		"Enable leader election for controller manager. "+
			"Enabling this will ensure there is only one active controller manager.")
	flag.BoolVar(&secureMetrics, "metrics-secure", true,
		"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
	flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
	flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
	flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
	flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
		"The directory that contains the metrics server certificate.")
	flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
	flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
	flag.BoolVar(&enableHTTP2, "enable-http2", false,
		"If set, HTTP/2 will be enabled for the metrics and webhook servers")
	opts := zap.Options{
		Development: true,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	// if the enable-http2 flag is false (the default), http/2 should be disabled
	// due to its vulnerabilities. More specifically, disabling http/2 will
	// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
	// Rapid Reset CVEs. For more information see:
	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
	// - https://github.com/advisories/GHSA-4374-p667-p6c8
	disableHTTP2 := func(c *tls.Config) {
		setupLog.Info("Disabling HTTP/2")
		c.NextProtos = []string{"http/1.1"}
	}

	if !enableHTTP2 {
		tlsOpts = append(tlsOpts, disableHTTP2)
	}

	// Initial webhook TLS options
	webhookTLSOpts := tlsOpts
	webhookServerOptions := webhook.Options{
		TLSOpts: webhookTLSOpts,
	}

	if len(webhookCertPath) > 0 {
		setupLog.Info("Initializing webhook certificate watcher using provided certificates",
			"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)

		webhookServerOptions.CertDir = webhookCertPath
		webhookServerOptions.CertName = webhookCertName
		webhookServerOptions.KeyName = webhookCertKey
	}

	webhookServer := webhook.NewServer(webhookServerOptions)

	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
	// More info:
	// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/server
	// - https://book.kubebuilder.io/reference/metrics.html
	metricsServerOptions := metricsserver.Options{
		BindAddress:   metricsAddr,
		SecureServing: secureMetrics,
		TLSOpts:       tlsOpts,
	}

	if secureMetrics {
		// FilterProvider is used to protect the metrics endpoint with authn/authz.
		// These configurations ensure that only authorized users and service accounts
		// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
		// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/filters#WithAuthenticationAndAuthorization
		metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
	}

	// If the certificate is not specified, controller-runtime will automatically
	// generate self-signed certificates for the metrics server. While convenient for development and testing,
	// this setup is not recommended for production.
	//
	// TODO(user): If you enable certManager, uncomment the following lines:
	// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
	// managed by cert-manager for the metrics server.
	// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
	if len(metricsCertPath) > 0 {
		setupLog.Info("Initializing metrics certificate watcher using provided certificates",
			"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)

		metricsServerOptions.CertDir = metricsCertPath
		metricsServerOptions.CertName = metricsCertName
		metricsServerOptions.KeyName = metricsCertKey
	}

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:                 scheme,
		Metrics:                metricsServerOptions,
		WebhookServer:          webhookServer,
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       "3e9f67a9.testproject.org",
		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	})
	if err != nil {
		setupLog.Error(err, "Failed to start manager")
		os.Exit(1)
	}

	if err := (&crewcontroller.CaptainReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Captain")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookcrewv1.SetupCaptainWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Captain")
			os.Exit(1)
		}
	}
	if err := (&shipcontroller.FrigateReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Frigate")
		os.Exit(1)
	}
	if err := (&shipcontroller.DestroyerReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Destroyer")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookshipv1.SetupDestroyerWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Destroyer")
			os.Exit(1)
		}
	}
	if err := (&shipcontroller.CruiserReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Cruiser")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookshipv2alpha1.SetupCruiserWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Cruiser")
			os.Exit(1)
		}
	}
	if err := (&seacreaturescontroller.KrakenReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Kraken")
		os.Exit(1)
	}
	if err := (&seacreaturescontroller.LeviathanReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Leviathan")
		os.Exit(1)
	}
	if err := (&foopolicycontroller.HealthCheckPolicyReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "HealthCheckPolicy")
		os.Exit(1)
	}
	if err := (&appscontroller.DeploymentReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Deployment")
		os.Exit(1)
	}
	if err := (&foocontroller.BarReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Bar")
		os.Exit(1)
	}
	if err := (&fizcontroller.BarReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Bar")
		os.Exit(1)
	}
	if err := (&certmanagercontroller.CertificateReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Certificate")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookcertmanagerv1.SetupIssuerWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Issuer")
			os.Exit(1)
		}
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookcorev1.SetupPodWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Pod")
			os.Exit(1)
		}
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookappsv1.SetupDeploymentWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Deployment")
			os.Exit(1)
		}
	}
	if err := (&examplecomcontroller.MemcachedReconciler{
		Client:   mgr.GetClient(),
		Scheme:   mgr.GetScheme(),
		Recorder: mgr.GetEventRecorder("memcached-controller"),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Memcached")
		os.Exit(1)
	}
	if err := (&examplecomcontroller.BusyboxReconciler{
		Client:   mgr.GetClient(),
		Scheme:   mgr.GetScheme(),
		Recorder: mgr.GetEventRecorder("busybox-controller"),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Busybox")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookexamplecomv1alpha1.SetupMemcachedWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Memcached")
			os.Exit(1)
		}
	}
	if err := (&examplecomcontroller.WordpressReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Wordpress")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookexamplecomv1.SetupWordpressWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Wordpress")
			os.Exit(1)
		}
	}
	// +kubebuilder:scaffold:builder

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up health check")
		os.Exit(1)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up ready check")
		os.Exit(1)
	}

	setupLog.Info("Starting manager")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "Failed to run manager")
		os.Exit(1)
	}
}


================================================
FILE: testdata/project-v4-multigroup/config/certmanager/certificate-metrics.yaml
================================================
# The following manifests contain a self-signed issuer CR and a metrics certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: metrics-certs  # this name should match the one appeared in kustomizeconfig.yaml
  namespace: system
spec:
  dnsNames:
  # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
  # replacements in the config/default/kustomization.yaml file.
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: metrics-server-cert


================================================
FILE: testdata/project-v4-multigroup/config/certmanager/certificate-webhook.yaml
================================================
# The following manifests contain a self-signed issuer CR and a certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: serving-cert  # this name should match the one appeared in kustomizeconfig.yaml
  namespace: system
spec:
  # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
  # replacements in the config/default/kustomization.yaml file.
  dnsNames:
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: webhook-server-cert


================================================
FILE: testdata/project-v4-multigroup/config/certmanager/issuer.yaml
================================================
# The following manifest contains a self-signed issuer CR.
# More information can be found at https://docs.cert-manager.io
# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes.
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: selfsigned-issuer
  namespace: system
spec:
  selfSigned: {}


================================================
FILE: testdata/project-v4-multigroup/config/certmanager/kustomization.yaml
================================================
resources:
- issuer.yaml
- certificate-webhook.yaml
- certificate-metrics.yaml

configurations:
- kustomizeconfig.yaml


================================================
FILE: testdata/project-v4-multigroup/config/certmanager/kustomizeconfig.yaml
================================================
# This configuration is for teaching kustomize how to update name ref substitution
nameReference:
- kind: Issuer
  group: cert-manager.io
  fieldSpecs:
  - kind: Certificate
    group: cert-manager.io
    path: spec/issuerRef/name


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/crew.testproject.org_captains.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: captains.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Captain
    listKind: CaptainList
    plural: captains
    singular: captain
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Captain is the Schema for the captains API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Captain
            properties:
              foo:
                description: foo is an example field of Captain. Edit captain_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Captain
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Captain resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_busyboxes.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: busyboxes.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Busybox
    listKind: BusyboxList
    plural: busyboxes
    singular: busybox
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Busybox is the Schema for the busyboxes API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Busybox
            properties:
              size:
                default: 1
                description: size defines the number of Busybox instances
                format: int32
                minimum: 0
                type: integer
            type: object
          status:
            description: status defines the observed state of Busybox
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Busybox resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_memcacheds.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: memcacheds.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Memcached
    listKind: MemcachedList
    plural: memcacheds
    singular: memcached
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Memcached is the Schema for the memcacheds API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Memcached
            properties:
              containerPort:
                description: containerPort defines the port that will be used to init
                  the container with the image
                format: int32
                type: integer
              size:
                default: 1
                description: size defines the number of Memcached instances
                format: int32
                minimum: 0
                type: integer
            required:
            - containerPort
            type: object
          status:
            description: status defines the observed state of Memcached
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Memcached resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/example.com.testproject.org_wordpresses.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: wordpresses.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Wordpress
    listKind: WordpressList
    plural: wordpresses
    singular: wordpress
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
  - name: v2
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: false
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/fiz.testproject.org_bars.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: bars.fiz.testproject.org
spec:
  group: fiz.testproject.org
  names:
    kind: Bar
    listKind: BarList
    plural: bars
    singular: bar
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Bar is the Schema for the bars API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Bar
            properties:
              foo:
                description: foo is an example field of Bar. Edit bar_types.go to
                  remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Bar
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Bar resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/foo.policy.testproject.org_healthcheckpolicies.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: healthcheckpolicies.foo.policy.testproject.org
spec:
  group: foo.policy.testproject.org
  names:
    kind: HealthCheckPolicy
    listKind: HealthCheckPolicyList
    plural: healthcheckpolicies
    singular: healthcheckpolicy
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: HealthCheckPolicy is the Schema for the healthcheckpolicies API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of HealthCheckPolicy
            properties:
              foo:
                description: foo is an example field of HealthCheckPolicy. Edit healthcheckpolicy_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of HealthCheckPolicy
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the HealthCheckPolicy resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/foo.testproject.org_bars.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: bars.foo.testproject.org
spec:
  group: foo.testproject.org
  names:
    kind: Bar
    listKind: BarList
    plural: bars
    singular: bar
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Bar is the Schema for the bars API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Bar
            properties:
              foo:
                description: foo is an example field of Bar. Edit bar_types.go to
                  remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Bar
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Bar resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/sea-creatures.testproject.org_krakens.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: krakens.sea-creatures.testproject.org
spec:
  group: sea-creatures.testproject.org
  names:
    kind: Kraken
    listKind: KrakenList
    plural: krakens
    singular: kraken
  scope: Namespaced
  versions:
  - name: v1beta1
    schema:
      openAPIV3Schema:
        description: Kraken is the Schema for the krakens API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Kraken
            properties:
              foo:
                description: foo is an example field of Kraken. Edit kraken_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Kraken
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Kraken resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/sea-creatures.testproject.org_leviathans.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: leviathans.sea-creatures.testproject.org
spec:
  group: sea-creatures.testproject.org
  names:
    kind: Leviathan
    listKind: LeviathanList
    plural: leviathans
    singular: leviathan
  scope: Namespaced
  versions:
  - name: v1beta2
    schema:
      openAPIV3Schema:
        description: Leviathan is the Schema for the leviathans API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Leviathan
            properties:
              foo:
                description: foo is an example field of Leviathan. Edit leviathan_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Leviathan
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Leviathan resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/ship.testproject.org_cruisers.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: cruisers.ship.testproject.org
spec:
  group: ship.testproject.org
  names:
    kind: Cruiser
    listKind: CruiserList
    plural: cruisers
    singular: cruiser
  scope: Cluster
  versions:
  - name: v2alpha1
    schema:
      openAPIV3Schema:
        description: Cruiser is the Schema for the cruisers API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Cruiser
            properties:
              foo:
                description: foo is an example field of Cruiser. Edit cruiser_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Cruiser
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Cruiser resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/ship.testproject.org_destroyers.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: destroyers.ship.testproject.org
spec:
  group: ship.testproject.org
  names:
    kind: Destroyer
    listKind: DestroyerList
    plural: destroyers
    singular: destroyer
  scope: Cluster
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Destroyer is the Schema for the destroyers API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Destroyer
            properties:
              foo:
                description: foo is an example field of Destroyer. Edit destroyer_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Destroyer
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Destroyer resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/bases/ship.testproject.org_frigates.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: frigates.ship.testproject.org
spec:
  group: ship.testproject.org
  names:
    kind: Frigate
    listKind: FrigateList
    plural: frigates
    singular: frigate
  scope: Namespaced
  versions:
  - name: v1beta1
    schema:
      openAPIV3Schema:
        description: Frigate is the Schema for the frigates API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Frigate
            properties:
              foo:
                description: foo is an example field of Frigate. Edit frigate_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Frigate
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Frigate resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-multigroup/config/crd/kustomization.yaml
================================================
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/crew.testproject.org_captains.yaml
- bases/ship.testproject.org_frigates.yaml
- bases/ship.testproject.org_destroyers.yaml
- bases/ship.testproject.org_cruisers.yaml
- bases/sea-creatures.testproject.org_krakens.yaml
- bases/sea-creatures.testproject.org_leviathans.yaml
- bases/foo.policy.testproject.org_healthcheckpolicies.yaml
- bases/foo.testproject.org_bars.yaml
- bases/fiz.testproject.org_bars.yaml
- bases/example.com.testproject.org_memcacheds.yaml
- bases/example.com.testproject.org_busyboxes.yaml
- bases/example.com.testproject.org_wordpresses.yaml
# +kubebuilder:scaffold:crdkustomizeresource

patches:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
- path: patches/webhook_in_example.com_wordpresses.yaml
# +kubebuilder:scaffold:crdkustomizewebhookpatch

# [WEBHOOK] To enable webhook, uncomment the following section
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml


================================================
FILE: testdata/project-v4-multigroup/config/crd/kustomizeconfig.yaml
================================================
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
  version: v1
  fieldSpecs:
  - kind: CustomResourceDefinition
    version: v1
    group: apiextensions.k8s.io
    path: spec/conversion/webhook/clientConfig/service/name

varReference:
- path: metadata/annotations


================================================
FILE: testdata/project-v4-multigroup/config/crd/patches/webhook_in_example.com_wordpresses.yaml
================================================
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: wordpresses.example.com.testproject.org
spec:
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        service:
          namespace: system
          name: webhook-service
          path: /convert
      conversionReviewVersions:
      - v1


================================================
FILE: testdata/project-v4-multigroup/config/default/cert_metrics_manager_patch.yaml
================================================
# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs.

# Add the volumeMount for the metrics-server certs
- op: add
  path: /spec/template/spec/containers/0/volumeMounts/-
  value:
    mountPath: /tmp/k8s-metrics-server/metrics-certs
    name: metrics-certs
    readOnly: true

# Add the --metrics-cert-path argument for the metrics server
- op: add
  path: /spec/template/spec/containers/0/args/-
  value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs

# Add the metrics-server certs volume configuration
- op: add
  path: /spec/template/spec/volumes/-
  value:
    name: metrics-certs
    secret:
      secretName: metrics-server-cert
      optional: false
      items:
        - key: ca.crt
          path: ca.crt
        - key: tls.crt
          path: tls.crt
        - key: tls.key
          path: tls.key


================================================
FILE: testdata/project-v4-multigroup/config/default/kustomization.yaml
================================================
# Adds namespace to all resources.
namespace: project-v4-multigroup-system

# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: project-v4-multigroup-

# Labels to add to all resources and selectors.
#labels:
#- includeSelectors: true
#  pairs:
#    someName: someValue

resources:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
# [METRICS] Expose the controller manager metrics service.
- metrics_service.yaml
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
# be able to communicate with the Webhook Server.
#- ../network-policy

# Uncomment the patches line if you enable Metrics
patches:
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
# More info: https://book.kubebuilder.io/reference/metrics
- path: manager_metrics_patch.yaml
  target:
    kind: Deployment

# Uncomment the patches line if you enable Metrics and CertManager
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
# This patch will protect the metrics with certManager self-signed certs.
#- path: cert_metrics_manager_patch.yaml
#  target:
#    kind: Deployment

# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- path: manager_webhook_patch.yaml
  target:
    kind: Deployment

# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
replacements:
# - source: # Uncomment the following block to enable certificates for metrics
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.name
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 0
#         create: true
#     - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 0
#         create: true

# - source:
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.namespace
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 1
#         create: true
#     - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 1
#         create: true

 - source: # Uncomment the following block if you have any webhook
     kind: Service
     version: v1
     name: webhook-service
     fieldPath: .metadata.name # Name of the service
   targets:
     - select:
         kind: Certificate
         group: cert-manager.io
         version: v1
         name: serving-cert
       fieldPaths:
         - .spec.dnsNames.0
         - .spec.dnsNames.1
       options:
         delimiter: '.'
         index: 0
         create: true
 - source:
     kind: Service
     version: v1
     name: webhook-service
     fieldPath: .metadata.namespace # Namespace of the service
   targets:
     - select:
         kind: Certificate
         group: cert-manager.io
         version: v1
         name: serving-cert
       fieldPaths:
         - .spec.dnsNames.0
         - .spec.dnsNames.1
       options:
         delimiter: '.'
         index: 1
         create: true

 - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert # This name should match the one in certificate.yaml
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets:
     - select:
         kind: ValidatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets:
     - select:
         kind: ValidatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true

 - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets:
     - select:
         kind: MutatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets:
     - select:
         kind: MutatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true

 - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
     - select:
         kind: CustomResourceDefinition
         name: wordpresses.example.com.testproject.org
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
# +kubebuilder:scaffold:crdkustomizecainjectionns
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
     - select:
         kind: CustomResourceDefinition
         name: wordpresses.example.com.testproject.org
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true
# +kubebuilder:scaffold:crdkustomizecainjectionname


================================================
FILE: testdata/project-v4-multigroup/config/default/manager_metrics_patch.yaml
================================================
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
- op: add
  path: /spec/template/spec/containers/0/args/0
  value: --metrics-bind-address=:8443


================================================
FILE: testdata/project-v4-multigroup/config/default/manager_webhook_patch.yaml
================================================
# This patch ensures the webhook certificates are properly mounted in the manager container.
# It configures the necessary arguments, volumes, volume mounts, and container ports.

# Add the --webhook-cert-path argument for configuring the webhook certificate path
- op: add
  path: /spec/template/spec/containers/0/args/-
  value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs

# Add the volumeMount for the webhook certificates
- op: add
  path: /spec/template/spec/containers/0/volumeMounts/-
  value:
    mountPath: /tmp/k8s-webhook-server/serving-certs
    name: webhook-certs
    readOnly: true

# Add the port configuration for the webhook server
- op: add
  path: /spec/template/spec/containers/0/ports/-
  value:
    containerPort: 9443
    name: webhook-server
    protocol: TCP

# Add the volume configuration for the webhook certificates
- op: add
  path: /spec/template/spec/volumes/-
  value:
    name: webhook-certs
    secret:
      secretName: webhook-server-cert


================================================
FILE: testdata/project-v4-multigroup/config/default/metrics_service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager-metrics-service
  namespace: system
spec:
  ports:
  - name: https
    port: 8443
    protocol: TCP
    targetPort: 8443
  selector:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-multigroup


================================================
FILE: testdata/project-v4-multigroup/config/manager/kustomization.yaml
================================================
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
  newName: controller
  newTag: latest


================================================
FILE: testdata/project-v4-multigroup/config/manager/manager.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: controller-manager
  namespace: system
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
spec:
  selector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-multigroup
  replicas: 1
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        control-plane: controller-manager
        app.kubernetes.io/name: project-v4-multigroup
    spec:
      # TODO(user): Uncomment the following code to configure the nodeAffinity expression
      # according to the platforms which are supported by your solution.
      # It is considered best practice to support multiple architectures. You can
      # build your manager image using the makefile target docker-buildx.
      # affinity:
      #   nodeAffinity:
      #     requiredDuringSchedulingIgnoredDuringExecution:
      #       nodeSelectorTerms:
      #         - matchExpressions:
      #           - key: kubernetes.io/arch
      #             operator: In
      #             values:
      #               - amd64
      #               - arm64
      #               - ppc64le
      #               - s390x
      #           - key: kubernetes.io/os
      #             operator: In
      #             values:
      #               - linux
      securityContext:
        # Projects are configured by default to adhere to the "restricted" Pod Security Standards.
        # This ensures that deployments meet the highest security requirements for Kubernetes.
        # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
      - command:
        - /manager
        args:
          - --leader-elect
          - --health-probe-bind-address=:8081
        image: controller:latest
        name: manager
        env:
        - name: BUSYBOX_IMAGE
          value: busybox:1.36.1
        - name: MEMCACHED_IMAGE
          value: memcached:1.6.26-alpine3.19
        ports: []
        securityContext:
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - "ALL"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 15
          periodSeconds: 20
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081
          initialDelaySeconds: 5
          periodSeconds: 10
        # TODO(user): Configure the resources accordingly based on the project requirements.
        # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        volumeMounts: []
      volumes: []
      serviceAccountName: controller-manager
      terminationGracePeriodSeconds: 10


================================================
FILE: testdata/project-v4-multigroup/config/network-policy/allow-metrics-traffic.yaml
================================================
# This NetworkPolicy allows ingress traffic
# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
# namespaces are able to gather data from the metrics endpoint.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: allow-metrics-traffic
  namespace: system
spec:
  podSelector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-multigroup
  policyTypes:
    - Ingress
  ingress:
    # This allows ingress traffic from any namespace with the label metrics: enabled
    - from:
      - namespaceSelector:
          matchLabels:
            metrics: enabled  # Only from namespaces with this label
      ports:
        - port: 8443
          protocol: TCP


================================================
FILE: testdata/project-v4-multigroup/config/network-policy/allow-webhook-traffic.yaml
================================================
# This NetworkPolicy allows ingress traffic to your webhook server running
# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks
# will only work when applied in namespaces labeled with 'webhook: enabled'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: allow-webhook-traffic
  namespace: system
spec:
  podSelector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-multigroup
  policyTypes:
    - Ingress
  ingress:
    # This allows ingress traffic from any namespace with the label webhook: enabled
    - from:
      - namespaceSelector:
          matchLabels:
            webhook: enabled # Only from namespaces with this label
      ports:
        - port: 443
          protocol: TCP


================================================
FILE: testdata/project-v4-multigroup/config/network-policy/kustomization.yaml
================================================
resources:
- allow-webhook-traffic.yaml
- allow-metrics-traffic.yaml


================================================
FILE: testdata/project-v4-multigroup/config/prometheus/kustomization.yaml
================================================
resources:
- monitor.yaml

# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
# to securely reference certificates created and managed by cert-manager.
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
# to mount the "metrics-server-cert" secret in the Manager Deployment.
#patches:
#  - path: monitor_tls_patch.yaml
#    target:
#      kind: ServiceMonitor


================================================
FILE: testdata/project-v4-multigroup/config/prometheus/monitor.yaml
================================================
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager-metrics-monitor
  namespace: system
spec:
  endpoints:
    - path: /metrics
      port: https # Ensure this is the name of the port that exposes HTTPS metrics
      scheme: https
      bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
      tlsConfig:
        # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
        # certificate verification, exposing the system to potential man-in-the-middle attacks.
        # For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
        # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
        # which securely references the certificate from the 'metrics-server-cert' secret.
        insecureSkipVerify: true
  selector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-multigroup


================================================
FILE: testdata/project-v4-multigroup/config/prometheus/monitor_tls_patch.yaml
================================================
# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
# using certificates managed by cert-manager
- op: replace
  path: /spec/endpoints/0/tlsConfig
  value:
    # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
    serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
    insecureSkipVerify: false
    ca:
      secret:
        name: metrics-server-cert
        key: ca.crt
    cert:
      secret:
        name: metrics-server-cert
        key: tls.crt
    keySecret:
      name: metrics-server-cert
      key: tls.key


================================================
FILE: testdata/project-v4-multigroup/config/rbac/crew_captain_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over crew.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: crew-captain-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/crew_captain_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the crew.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: crew-captain-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/crew_captain_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to crew.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: crew-captain-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_busybox_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over example.com.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-busybox-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_busybox_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the example.com.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-busybox-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_busybox_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to example.com.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-busybox-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_memcached_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over example.com.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-memcached-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_memcached_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the example.com.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-memcached-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_memcached_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to example.com.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-memcached-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_wordpress_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over example.com.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-wordpress-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_wordpress_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the example.com.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-wordpress-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/example.com_wordpress_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to example.com.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: example.com-wordpress-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/fiz_bar_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over fiz.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: fiz-bar-admin-role
rules:
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars
  verbs:
  - '*'
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/fiz_bar_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the fiz.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: fiz-bar-editor-role
rules:
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/fiz_bar_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to fiz.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: fiz-bar-viewer-role
rules:
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/foo.policy_healthcheckpolicy_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over foo.policy.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: foo.policy-healthcheckpolicy-admin-role
rules:
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - '*'
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/foo.policy_healthcheckpolicy_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the foo.policy.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: foo.policy-healthcheckpolicy-editor-role
rules:
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/foo.policy_healthcheckpolicy_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to foo.policy.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: foo.policy-healthcheckpolicy-viewer-role
rules:
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/foo_bar_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over foo.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: foo-bar-admin-role
rules:
- apiGroups:
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - '*'
- apiGroups:
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/foo_bar_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the foo.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: foo-bar-editor-role
rules:
- apiGroups:
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/foo_bar_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to foo.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: foo-bar-viewer-role
rules:
- apiGroups:
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/kustomization.yaml
================================================
resources:
# All RBAC will be applied under this service account in
# the deployment namespace. You may comment out this resource
# if your manager will use a service account that exists at
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
# subjects if changing service account names.
- service_account.yaml
- role.yaml
- role_binding.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# The following RBAC configurations are used to protect
# the metrics endpoint with authn/authz. These configurations
# ensure that only authorized users and service accounts
# can access the metrics endpoint. Comment the following
# permissions if you want to disable this protection.
# More info: https://book.kubebuilder.io/reference/metrics.html
- metrics_auth_role.yaml
- metrics_auth_role_binding.yaml
- metrics_reader_role.yaml
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the project-v4-multigroup itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- example.com_wordpress_admin_role.yaml
- example.com_wordpress_editor_role.yaml
- example.com_wordpress_viewer_role.yaml
- example.com_busybox_admin_role.yaml
- example.com_busybox_editor_role.yaml
- example.com_busybox_viewer_role.yaml
- example.com_memcached_admin_role.yaml
- example.com_memcached_editor_role.yaml
- example.com_memcached_viewer_role.yaml
- fiz_bar_admin_role.yaml
- fiz_bar_editor_role.yaml
- fiz_bar_viewer_role.yaml
- foo_bar_admin_role.yaml
- foo_bar_editor_role.yaml
- foo_bar_viewer_role.yaml
- foo.policy_healthcheckpolicy_admin_role.yaml
- foo.policy_healthcheckpolicy_editor_role.yaml
- foo.policy_healthcheckpolicy_viewer_role.yaml
- sea-creatures_leviathan_admin_role.yaml
- sea-creatures_leviathan_editor_role.yaml
- sea-creatures_leviathan_viewer_role.yaml
- sea-creatures_kraken_admin_role.yaml
- sea-creatures_kraken_editor_role.yaml
- sea-creatures_kraken_viewer_role.yaml
- ship_cruiser_admin_role.yaml
- ship_cruiser_editor_role.yaml
- ship_cruiser_viewer_role.yaml
- ship_destroyer_admin_role.yaml
- ship_destroyer_editor_role.yaml
- ship_destroyer_viewer_role.yaml
- ship_frigate_admin_role.yaml
- ship_frigate_editor_role.yaml
- ship_frigate_viewer_role.yaml
- crew_captain_admin_role.yaml
- crew_captain_editor_role.yaml
- crew_captain_viewer_role.yaml



================================================
FILE: testdata/project-v4-multigroup/config/rbac/leader_election_role.yaml
================================================
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: leader-election-role
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch


================================================
FILE: testdata/project-v4-multigroup/config/rbac/leader_election_role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: leader-election-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: leader-election-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-multigroup/config/rbac/metrics_auth_role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: metrics-auth-role
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create


================================================
FILE: testdata/project-v4-multigroup/config/rbac/metrics_auth_role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: metrics-auth-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: metrics-auth-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-multigroup/config/rbac/metrics_reader_role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: metrics-reader
rules:
- nonResourceURLs:
  - "/metrics"
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/role.yaml
================================================
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: manager-role
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - apps
  resources:
  - deployments/finalizers
  verbs:
  - update
- apiGroups:
  - apps
  resources:
  - deployments/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - cert-manager.io
  resources:
  - certificates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/finalizers
  verbs:
  - update
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/finalizers
  verbs:
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - events.k8s.io
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  - memcacheds
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/finalizers
  - memcacheds/finalizers
  - wordpresses/finalizers
  verbs:
  - update
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  - memcacheds/status
  - wordpresses/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - fiz.testproject.org
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - fiz.testproject.org
  - foo.testproject.org
  resources:
  - bars/finalizers
  verbs:
  - update
- apiGroups:
  - fiz.testproject.org
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/finalizers
  verbs:
  - update
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  - leviathans
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/finalizers
  - leviathans/finalizers
  verbs:
  - update
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  - leviathans/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  - destroyers
  - frigates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/finalizers
  - destroyers/finalizers
  - frigates/finalizers
  verbs:
  - update
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  - destroyers/status
  - frigates/status
  verbs:
  - get
  - patch
  - update


================================================
FILE: testdata/project-v4-multigroup/config/rbac/role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: manager-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-multigroup/config/rbac/sea-creatures_kraken_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over sea-creatures.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: sea-creatures-kraken-admin-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  verbs:
  - '*'
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/sea-creatures_kraken_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the sea-creatures.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: sea-creatures-kraken-editor-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/sea-creatures_kraken_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to sea-creatures.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: sea-creatures-kraken-viewer-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/sea-creatures_leviathan_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over sea-creatures.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: sea-creatures-leviathan-admin-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans
  verbs:
  - '*'
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/sea-creatures_leviathan_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the sea-creatures.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: sea-creatures-leviathan-editor-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/sea-creatures_leviathan_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to sea-creatures.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: sea-creatures-leviathan-viewer-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/service_account.yaml
================================================
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_cruiser_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over ship.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-cruiser-admin-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  verbs:
  - '*'
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_cruiser_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the ship.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-cruiser-editor-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_cruiser_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to ship.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-cruiser-viewer-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_destroyer_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over ship.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-destroyer-admin-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers
  verbs:
  - '*'
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_destroyer_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the ship.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-destroyer-editor-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_destroyer_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to ship.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-destroyer-viewer-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_frigate_admin_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over ship.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-frigate-admin-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates
  verbs:
  - '*'
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_frigate_editor_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the ship.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-frigate-editor-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/rbac/ship_frigate_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-multigroup itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to ship.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: ship-frigate-viewer-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-multigroup/config/samples/crew_v1_captain.yaml
================================================
apiVersion: crew.testproject.org/v1
kind: Captain
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: captain-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/example.com_v1_wordpress.yaml
================================================
apiVersion: example.com.testproject.org/v1
kind: Wordpress
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: wordpress-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_busybox.yaml
================================================
apiVersion: example.com.testproject.org/v1alpha1
kind: Busybox
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: busybox-sample
spec:
  # TODO(user): edit the following value to ensure the number
  # of Pods/Instances your Operand must have on cluster
  size: 1


================================================
FILE: testdata/project-v4-multigroup/config/samples/example.com_v1alpha1_memcached.yaml
================================================
apiVersion: example.com.testproject.org/v1alpha1
kind: Memcached
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: memcached-sample
spec:
  # TODO(user): edit the following value to ensure the number
  # of Pods/Instances your Operand must have on cluster
  size: 1

  # TODO(user): edit the following value to ensure the container has the right port to be initialized
  containerPort: 11211


================================================
FILE: testdata/project-v4-multigroup/config/samples/example.com_v2_wordpress.yaml
================================================
apiVersion: example.com.testproject.org/v2
kind: Wordpress
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: wordpress-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/fiz_v1_bar.yaml
================================================
apiVersion: fiz.testproject.org/v1
kind: Bar
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: bar-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/foo.policy_v1_healthcheckpolicy.yaml
================================================
apiVersion: foo.policy.testproject.org/v1
kind: HealthCheckPolicy
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: healthcheckpolicy-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/foo_v1_bar.yaml
================================================
apiVersion: foo.testproject.org/v1
kind: Bar
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: bar-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/kustomization.yaml
================================================
## Append samples of your project ##
resources:
- crew_v1_captain.yaml
- ship_v1beta1_frigate.yaml
- ship_v1_destroyer.yaml
- ship_v2alpha1_cruiser.yaml
- sea-creatures_v1beta1_kraken.yaml
- sea-creatures_v1beta2_leviathan.yaml
- foo.policy_v1_healthcheckpolicy.yaml
- foo_v1_bar.yaml
- fiz_v1_bar.yaml
- example.com_v1alpha1_memcached.yaml
- example.com_v1alpha1_busybox.yaml
- example.com_v1_wordpress.yaml
- example.com_v2_wordpress.yaml
# +kubebuilder:scaffold:manifestskustomizesamples


================================================
FILE: testdata/project-v4-multigroup/config/samples/sea-creatures_v1beta1_kraken.yaml
================================================
apiVersion: sea-creatures.testproject.org/v1beta1
kind: Kraken
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: kraken-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/sea-creatures_v1beta2_leviathan.yaml
================================================
apiVersion: sea-creatures.testproject.org/v1beta2
kind: Leviathan
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: leviathan-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/ship_v1_destroyer.yaml
================================================
apiVersion: ship.testproject.org/v1
kind: Destroyer
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: destroyer-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/ship_v1beta1_frigate.yaml
================================================
apiVersion: ship.testproject.org/v1beta1
kind: Frigate
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: frigate-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/samples/ship_v2alpha1_cruiser.yaml
================================================
apiVersion: ship.testproject.org/v2alpha1
kind: Cruiser
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: cruiser-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-multigroup/config/webhook/kustomization.yaml
================================================
resources:
- manifests.yaml
- service.yaml


================================================
FILE: testdata/project-v4-multigroup/config/webhook/manifests.yaml
================================================
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: mcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-apps-v1-deployment
  failurePolicy: Fail
  name: mdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-ship-testproject-org-v1-destroyer
  failurePolicy: Fail
  name: mdestroyer-v1.kb.io
  rules:
  - apiGroups:
    - ship.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - destroyers
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /mutate-cert-manager-io-v1-issuer
  failurePolicy: Fail
  name: missuer-v1.kb.io
  rules:
  - apiGroups:
    - cert-manager.io
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - issuers
  sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: vcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate-ship-testproject-org-v2alpha1-cruiser
  failurePolicy: Fail
  name: vcruiser-v2alpha1.kb.io
  rules:
  - apiGroups:
    - ship.testproject.org
    apiVersions:
    - v2alpha1
    operations:
    - CREATE
    - UPDATE
    resources:
    - cruisers
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate-apps-v1-deployment
  failurePolicy: Fail
  name: vdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate-example-com-testproject-org-v1alpha1-memcached
  failurePolicy: Fail
  name: vmemcached-v1alpha1.kb.io
  rules:
  - apiGroups:
    - example.com.testproject.org
    apiVersions:
    - v1alpha1
    operations:
    - CREATE
    - UPDATE
    resources:
    - memcacheds
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate--v1-pod
  failurePolicy: Fail
  name: vpod-v1.kb.io
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - pods
  sideEffects: None


================================================
FILE: testdata/project-v4-multigroup/config/webhook/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: project-v4-multigroup
    app.kubernetes.io/managed-by: kustomize
  name: webhook-service
  namespace: system
spec:
  ports:
    - port: 443
      protocol: TCP
      targetPort: 9443
  selector:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-multigroup


================================================
FILE: testdata/project-v4-multigroup/dist/install.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
    control-plane: controller-manager
  name: project-v4-multigroup-system
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: bars.fiz.testproject.org
spec:
  group: fiz.testproject.org
  names:
    kind: Bar
    listKind: BarList
    plural: bars
    singular: bar
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Bar is the Schema for the bars API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Bar
            properties:
              foo:
                description: foo is an example field of Bar. Edit bar_types.go to
                  remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Bar
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Bar resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: bars.foo.testproject.org
spec:
  group: foo.testproject.org
  names:
    kind: Bar
    listKind: BarList
    plural: bars
    singular: bar
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Bar is the Schema for the bars API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Bar
            properties:
              foo:
                description: foo is an example field of Bar. Edit bar_types.go to
                  remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Bar
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Bar resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: busyboxes.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Busybox
    listKind: BusyboxList
    plural: busyboxes
    singular: busybox
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Busybox is the Schema for the busyboxes API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Busybox
            properties:
              size:
                default: 1
                description: size defines the number of Busybox instances
                format: int32
                minimum: 0
                type: integer
            type: object
          status:
            description: status defines the observed state of Busybox
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Busybox resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: captains.crew.testproject.org
spec:
  group: crew.testproject.org
  names:
    kind: Captain
    listKind: CaptainList
    plural: captains
    singular: captain
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Captain is the Schema for the captains API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Captain
            properties:
              foo:
                description: foo is an example field of Captain. Edit captain_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Captain
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Captain resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: cruisers.ship.testproject.org
spec:
  group: ship.testproject.org
  names:
    kind: Cruiser
    listKind: CruiserList
    plural: cruisers
    singular: cruiser
  scope: Cluster
  versions:
  - name: v2alpha1
    schema:
      openAPIV3Schema:
        description: Cruiser is the Schema for the cruisers API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Cruiser
            properties:
              foo:
                description: foo is an example field of Cruiser. Edit cruiser_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Cruiser
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Cruiser resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: destroyers.ship.testproject.org
spec:
  group: ship.testproject.org
  names:
    kind: Destroyer
    listKind: DestroyerList
    plural: destroyers
    singular: destroyer
  scope: Cluster
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Destroyer is the Schema for the destroyers API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Destroyer
            properties:
              foo:
                description: foo is an example field of Destroyer. Edit destroyer_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Destroyer
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Destroyer resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: frigates.ship.testproject.org
spec:
  group: ship.testproject.org
  names:
    kind: Frigate
    listKind: FrigateList
    plural: frigates
    singular: frigate
  scope: Namespaced
  versions:
  - name: v1beta1
    schema:
      openAPIV3Schema:
        description: Frigate is the Schema for the frigates API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Frigate
            properties:
              foo:
                description: foo is an example field of Frigate. Edit frigate_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Frigate
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Frigate resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: healthcheckpolicies.foo.policy.testproject.org
spec:
  group: foo.policy.testproject.org
  names:
    kind: HealthCheckPolicy
    listKind: HealthCheckPolicyList
    plural: healthcheckpolicies
    singular: healthcheckpolicy
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: HealthCheckPolicy is the Schema for the healthcheckpolicies API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of HealthCheckPolicy
            properties:
              foo:
                description: foo is an example field of HealthCheckPolicy. Edit healthcheckpolicy_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of HealthCheckPolicy
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the HealthCheckPolicy resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: krakens.sea-creatures.testproject.org
spec:
  group: sea-creatures.testproject.org
  names:
    kind: Kraken
    listKind: KrakenList
    plural: krakens
    singular: kraken
  scope: Namespaced
  versions:
  - name: v1beta1
    schema:
      openAPIV3Schema:
        description: Kraken is the Schema for the krakens API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Kraken
            properties:
              foo:
                description: foo is an example field of Kraken. Edit kraken_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Kraken
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Kraken resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: leviathans.sea-creatures.testproject.org
spec:
  group: sea-creatures.testproject.org
  names:
    kind: Leviathan
    listKind: LeviathanList
    plural: leviathans
    singular: leviathan
  scope: Namespaced
  versions:
  - name: v1beta2
    schema:
      openAPIV3Schema:
        description: Leviathan is the Schema for the leviathans API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Leviathan
            properties:
              foo:
                description: foo is an example field of Leviathan. Edit leviathan_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Leviathan
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Leviathan resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: memcacheds.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Memcached
    listKind: MemcachedList
    plural: memcacheds
    singular: memcached
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Memcached is the Schema for the memcacheds API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Memcached
            properties:
              containerPort:
                description: containerPort defines the port that will be used to init
                  the container with the image
                format: int32
                type: integer
              size:
                default: 1
                description: size defines the number of Memcached instances
                format: int32
                minimum: 0
                type: integer
            required:
            - containerPort
            type: object
          status:
            description: status defines the observed state of Memcached
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Memcached resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-multigroup-system/project-v4-multigroup-serving-cert
    controller-gen.kubebuilder.io/version: v0.20.1
  name: wordpresses.example.com.testproject.org
spec:
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        service:
          name: project-v4-multigroup-webhook-service
          namespace: project-v4-multigroup-system
          path: /convert
      conversionReviewVersions:
      - v1
  group: example.com.testproject.org
  names:
    kind: Wordpress
    listKind: WordpressList
    plural: wordpresses
    singular: wordpress
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
  - name: v2
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: false
    subresources:
      status: {}
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-controller-manager
  namespace: project-v4-multigroup-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-leader-election-role
  namespace: project-v4-multigroup-system
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-crew-captain-admin-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - '*'
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-crew-captain-editor-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-crew-captain-viewer-role
rules:
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-busybox-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-busybox-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-busybox-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-memcached-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-memcached-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-memcached-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-wordpress-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-wordpress-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-example.com-wordpress-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-fiz-bar-admin-role
rules:
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars
  verbs:
  - '*'
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-fiz-bar-editor-role
rules:
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-fiz-bar-viewer-role
rules:
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - fiz.testproject.org
  resources:
  - bars/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-foo-bar-admin-role
rules:
- apiGroups:
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - '*'
- apiGroups:
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-foo-bar-editor-role
rules:
- apiGroups:
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-foo-bar-viewer-role
rules:
- apiGroups:
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-foo.policy-healthcheckpolicy-admin-role
rules:
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - '*'
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-foo.policy-healthcheckpolicy-editor-role
rules:
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-foo.policy-healthcheckpolicy-viewer-role
rules:
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-multigroup-manager-role
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - apps
  resources:
  - deployments/finalizers
  verbs:
  - update
- apiGroups:
  - apps
  resources:
  - deployments/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - cert-manager.io
  resources:
  - certificates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/finalizers
  verbs:
  - update
- apiGroups:
  - cert-manager.io
  resources:
  - certificates/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - captains
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/finalizers
  verbs:
  - update
- apiGroups:
  - crew.testproject.org
  resources:
  - captains/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - events.k8s.io
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  - memcacheds
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/finalizers
  - memcacheds/finalizers
  - wordpresses/finalizers
  verbs:
  - update
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  - memcacheds/status
  - wordpresses/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - fiz.testproject.org
  - foo.testproject.org
  resources:
  - bars
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - fiz.testproject.org
  - foo.testproject.org
  resources:
  - bars/finalizers
  verbs:
  - update
- apiGroups:
  - fiz.testproject.org
  - foo.testproject.org
  resources:
  - bars/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/finalizers
  verbs:
  - update
- apiGroups:
  - foo.policy.testproject.org
  resources:
  - healthcheckpolicies/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  - leviathans
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/finalizers
  - leviathans/finalizers
  verbs:
  - update
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  - leviathans/status
  verbs:
  - get
  - patch
  - update
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  - destroyers
  - frigates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/finalizers
  - destroyers/finalizers
  - frigates/finalizers
  verbs:
  - update
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  - destroyers/status
  - frigates/status
  verbs:
  - get
  - patch
  - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-multigroup-metrics-auth-role
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-multigroup-metrics-reader
rules:
- nonResourceURLs:
  - /metrics
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-sea-creatures-kraken-admin-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  verbs:
  - '*'
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-sea-creatures-kraken-editor-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-sea-creatures-kraken-viewer-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - krakens/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-sea-creatures-leviathan-admin-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans
  verbs:
  - '*'
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-sea-creatures-leviathan-editor-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-sea-creatures-leviathan-viewer-role
rules:
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - sea-creatures.testproject.org
  resources:
  - leviathans/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-cruiser-admin-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  verbs:
  - '*'
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-cruiser-editor-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-cruiser-viewer-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - cruisers/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-destroyer-admin-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers
  verbs:
  - '*'
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-destroyer-editor-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-destroyer-viewer-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - destroyers/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-frigate-admin-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates
  verbs:
  - '*'
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-frigate-editor-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-ship-frigate-viewer-role
rules:
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - ship.testproject.org
  resources:
  - frigates/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-leader-election-rolebinding
  namespace: project-v4-multigroup-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: project-v4-multigroup-leader-election-role
subjects:
- kind: ServiceAccount
  name: project-v4-multigroup-controller-manager
  namespace: project-v4-multigroup-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: project-v4-multigroup-manager-role
subjects:
- kind: ServiceAccount
  name: project-v4-multigroup-controller-manager
  namespace: project-v4-multigroup-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: project-v4-multigroup-metrics-auth-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: project-v4-multigroup-metrics-auth-role
subjects:
- kind: ServiceAccount
  name: project-v4-multigroup-controller-manager
  namespace: project-v4-multigroup-system
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
    control-plane: controller-manager
  name: project-v4-multigroup-controller-manager-metrics-service
  namespace: project-v4-multigroup-system
spec:
  ports:
  - name: https
    port: 8443
    protocol: TCP
    targetPort: 8443
  selector:
    app.kubernetes.io/name: project-v4-multigroup
    control-plane: controller-manager
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-webhook-service
  namespace: project-v4-multigroup-system
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: 9443
  selector:
    app.kubernetes.io/name: project-v4-multigroup
    control-plane: controller-manager
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
    control-plane: controller-manager
  name: project-v4-multigroup-controller-manager
  namespace: project-v4-multigroup-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: project-v4-multigroup
      control-plane: controller-manager
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        app.kubernetes.io/name: project-v4-multigroup
        control-plane: controller-manager
    spec:
      containers:
      - args:
        - --metrics-bind-address=:8443
        - --leader-elect
        - --health-probe-bind-address=:8081
        - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs
        command:
        - /manager
        env:
        - name: BUSYBOX_IMAGE
          value: busybox:1.36.1
        - name: MEMCACHED_IMAGE
          value: memcached:1.6.26-alpine3.19
        image: controller:latest
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 15
          periodSeconds: 20
        name: manager
        ports:
        - containerPort: 9443
          name: webhook-server
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
          readOnlyRootFilesystem: true
        volumeMounts:
        - mountPath: /tmp/k8s-webhook-server/serving-certs
          name: webhook-certs
          readOnly: true
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      serviceAccountName: project-v4-multigroup-controller-manager
      terminationGracePeriodSeconds: 10
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-server-cert
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-metrics-certs
  namespace: project-v4-multigroup-system
spec:
  dnsNames:
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: project-v4-multigroup-selfsigned-issuer
  secretName: metrics-server-cert
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-serving-cert
  namespace: project-v4-multigroup-system
spec:
  dnsNames:
  - project-v4-multigroup-webhook-service.project-v4-multigroup-system.svc
  - project-v4-multigroup-webhook-service.project-v4-multigroup-system.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: project-v4-multigroup-selfsigned-issuer
  secretName: webhook-server-cert
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-multigroup
  name: project-v4-multigroup-selfsigned-issuer
  namespace: project-v4-multigroup-system
spec:
  selfSigned: {}
---
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-multigroup-system/project-v4-multigroup-serving-cert
  name: project-v4-multigroup-mutating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /mutate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: mcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /mutate-apps-v1-deployment
  failurePolicy: Fail
  name: mdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /mutate-ship-testproject-org-v1-destroyer
  failurePolicy: Fail
  name: mdestroyer-v1.kb.io
  rules:
  - apiGroups:
    - ship.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - destroyers
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /mutate-cert-manager-io-v1-issuer
  failurePolicy: Fail
  name: missuer-v1.kb.io
  rules:
  - apiGroups:
    - cert-manager.io
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - issuers
  sideEffects: None
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-multigroup-system/project-v4-multigroup-serving-cert
  name: project-v4-multigroup-validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /validate-crew-testproject-org-v1-captain
  failurePolicy: Fail
  name: vcaptain-v1.kb.io
  rules:
  - apiGroups:
    - crew.testproject.org
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - captains
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /validate-ship-testproject-org-v2alpha1-cruiser
  failurePolicy: Fail
  name: vcruiser-v2alpha1.kb.io
  rules:
  - apiGroups:
    - ship.testproject.org
    apiVersions:
    - v2alpha1
    operations:
    - CREATE
    - UPDATE
    resources:
    - cruisers
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /validate-apps-v1-deployment
  failurePolicy: Fail
  name: vdeployment-v1.kb.io
  rules:
  - apiGroups:
    - apps
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - deployments
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /validate-example-com-testproject-org-v1alpha1-memcached
  failurePolicy: Fail
  name: vmemcached-v1alpha1.kb.io
  rules:
  - apiGroups:
    - example.com.testproject.org
    apiVersions:
    - v1alpha1
    operations:
    - CREATE
    - UPDATE
    resources:
    - memcacheds
  sideEffects: None
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-multigroup-webhook-service
      namespace: project-v4-multigroup-system
      path: /validate--v1-pod
  failurePolicy: Fail
  name: vpod-v1.kb.io
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - pods
  sideEffects: None


================================================
FILE: testdata/project-v4-multigroup/go.mod
================================================
module sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup

go 1.25.3

require (
	github.com/cert-manager/cert-manager v1.20.0
	github.com/onsi/ginkgo/v2 v2.28.0
	github.com/onsi/gomega v1.39.1
	k8s.io/api v0.35.2
	k8s.io/apimachinery v0.35.2
	k8s.io/client-go v0.35.2
	k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2
	sigs.k8s.io/controller-runtime v0.23.3
)

require (
	cel.dev/expr v0.25.1 // indirect
	github.com/Masterminds/semver/v3 v3.4.0 // indirect
	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
	github.com/beorn7/perks v1.0.1 // indirect
	github.com/blang/semver/v4 v4.0.0 // indirect
	github.com/cenkalti/backoff/v5 v5.0.3 // 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.13.0 // indirect
	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // 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-logr/zapr v1.3.0 // indirect
	github.com/go-openapi/jsonpointer v0.22.4 // indirect
	github.com/go-openapi/jsonreference v0.21.4 // indirect
	github.com/go-openapi/swag v0.23.1 // indirect
	github.com/go-openapi/swag/jsonname v0.25.4 // indirect
	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
	github.com/google/btree v1.1.3 // indirect
	github.com/google/cel-go v0.26.0 // indirect
	github.com/google/gnostic-models v0.7.1 // indirect
	github.com/google/go-cmp v0.7.0 // indirect
	github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect
	github.com/google/uuid v1.6.0 // indirect
	github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // 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.9.1 // 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.17.0 // indirect
	github.com/spf13/cobra v1.10.2 // indirect
	github.com/spf13/pflag v1.0.10 // indirect
	github.com/stoewer/go-strcase v1.3.1 // indirect
	github.com/x448/float16 v0.8.4 // indirect
	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
	go.opentelemetry.io/otel v1.40.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
	go.opentelemetry.io/otel/metric v1.40.0 // indirect
	go.opentelemetry.io/otel/sdk v1.40.0 // indirect
	go.opentelemetry.io/otel/trace v1.40.0 // indirect
	go.opentelemetry.io/proto/otlp v1.7.0 // indirect
	go.uber.org/multierr v1.11.0 // indirect
	go.uber.org/zap v1.27.1 // 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-20250718183923-645b1fa84792 // indirect
	golang.org/x/mod v0.32.0 // indirect
	golang.org/x/net v0.51.0 // indirect
	golang.org/x/oauth2 v0.35.0 // indirect
	golang.org/x/sync v0.19.0 // indirect
	golang.org/x/sys v0.41.0 // indirect
	golang.org/x/term v0.40.0 // indirect
	golang.org/x/text v0.34.0 // indirect
	golang.org/x/time v0.14.0 // indirect
	golang.org/x/tools v0.41.0 // indirect
	gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
	google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
	google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
	google.golang.org/grpc v1.79.1 // indirect
	google.golang.org/protobuf v1.36.11 // indirect
	gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
	gopkg.in/inf.v0 v0.9.1 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
	k8s.io/apiextensions-apiserver v0.35.2 // indirect
	k8s.io/apiserver v0.35.2 // indirect
	k8s.io/component-base v0.35.2 // indirect
	k8s.io/klog/v2 v2.140.0 // indirect
	k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect
	sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect
	sigs.k8s.io/gateway-api v1.5.0 // 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.2 // indirect
	sigs.k8s.io/yaml v1.6.0 // indirect
)


================================================
FILE: testdata/project-v4-multigroup/grafana/controller-resources-metrics.json
================================================
{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "1.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "percent"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 0
      },
      "id": 2,
      "interval": "1m",
      "links": [],
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "rate(process_cpu_seconds_total{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}[5m]) * 100",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "Pod: {{pod}} | Container: {{container}}",
          "refId": "A",
          "step": 10
        }
      ],
      "title": "Controller CPU Usage",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "bytes"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 0
      },
      "id": 4,
      "interval": "1m",
      "links": [],
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "process_resident_memory_bytes{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "Pod: {{pod}} | Container: {{container}}",
          "refId": "A",
          "step": 10
        }
      ],
      "title": "Controller Memory Usage",
      "type": "timeseries"
    }
  ],
  "refresh": "",
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "job",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "observability",
          "value": "observability"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total, namespace)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "namespace",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total, namespace)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "All",
          "value": "$__all"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
        "hide": 2,
        "includeAll": true,
        "label": "pod",
        "multi": true,
        "name": "pod",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Controller-Resources-Metrics",
  "weekStart": ""
}


================================================
FILE: testdata/project-v4-multigroup/grafana/controller-runtime-metrics.json
================================================
{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "1.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "datasource",
          "uid": "grafana"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "collapsed": false,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 0
      },
      "id": 9,
      "panels": [],
      "title": "Reconciliation Metrics",
      "type": "row"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 3,
        "x": 0,
        "y": 1
      },
      "id": 24,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "controller_runtime_active_workers{job=\"$job\", namespace=\"$namespace\"}",
          "interval": "",
          "legendFormat": "{{controller}} {{instance}}",
          "refId": "A"
        }
      ],
      "title": "Number of workers in use",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of reconciliations per controller",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "cpm"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 11,
        "x": 3,
        "y": 1
      },
      "id": 7,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "editorMode": "code",
          "exemplar": true,
          "expr": "sum(rate(controller_runtime_reconcile_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
          "interval": "",
          "legendFormat": "{{instance}} {{pod}}",
          "range": true,
          "refId": "A"
        }
      ],
      "title": "Total Reconciliation Count Per Controller",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of reconciliation errors per controller",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "cpm"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 10,
        "x": 14,
        "y": 1
      },
      "id": 6,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "editorMode": "code",
          "exemplar": true,
          "expr": "sum(rate(controller_runtime_reconcile_errors_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
          "interval": "",
          "legendFormat": "{{instance}} {{pod}}",
          "range": true,
          "refId": "A"
        }
      ],
      "title": "Reconciliation Error Count Per Controller",
      "type": "timeseries"
    },
    {
      "collapsed": false,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 9
      },
      "id": 11,
      "panels": [],
      "title": "Work Queue Metrics",
      "type": "row"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 3,
        "x": 0,
        "y": 10
      },
      "id": 22,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "workqueue_depth{job=\"$job\", namespace=\"$namespace\"}",
          "interval": "",
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "WorkQueue Depth",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How long in seconds an item stays in workqueue before being requested",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 10,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "normal"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 11,
        "x": 3,
        "y": 10
      },
      "id": 13,
      "options": {
        "legend": {
          "calcs": [
            "max",
            "mean"
          ],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.50, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "interval": "",
          "legendFormat": "P50 {{name}} {{instance}} ",
          "refId": "A"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.90, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P90 {{name}} {{instance}} ",
          "refId": "B"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.99, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P99 {{name}} {{instance}} ",
          "refId": "C"
        }
      ],
      "title": "Seconds For Items Stay In Queue (before being requested) (P50, P90, P99)",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "ops"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 10,
        "x": 14,
        "y": 10
      },
      "id": 15,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "sum(rate(workqueue_adds_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
          "interval": "",
          "legendFormat": "{{name}} {{instance}}",
          "refId": "A"
        }
      ],
      "title": "Work Queue Add Rate",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How many seconds of work has done that is in progress and hasn't been observed by work_duration.\nLarge values indicate stuck threads.\nOne can deduce the number of stuck threads by observing the rate at which this increases.",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 3,
        "x": 0,
        "y": 18
      },
      "id": 23,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "rate(workqueue_unfinished_work_seconds{job=\"$job\", namespace=\"$namespace\"}[5m])",
          "interval": "",
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "Unfinished Seconds",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How long in seconds processing an item from workqueue takes.",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 10,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 11,
        "x": 3,
        "y": 18
      },
      "id": 19,
      "options": {
        "legend": {
          "calcs": [
            "max",
            "mean"
          ],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.50, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "interval": "",
          "legendFormat": "P50 {{name}} {{instance}} ",
          "refId": "A"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.90, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P90 {{name}} {{instance}} ",
          "refId": "B"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.99, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P99 {{name}} {{instance}} ",
          "refId": "C"
        }
      ],
      "title": "Seconds Processing Items From WorkQueue (P50, P90, P99)",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of retries handled by workqueue",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "ops"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 10,
        "x": 14,
        "y": 18
      },
      "id": 17,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "sum(rate(workqueue_retries_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
          "interval": "",
          "legendFormat": "{{name}} {{instance}} ",
          "refId": "A"
        }
      ],
      "title": "Work Queue Retries Rate",
      "type": "timeseries"
    }
  ],
  "refresh": "",
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "job",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total, namespace)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "namespace",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total, namespace)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": true,
          "text": [
            "All"
          ],
          "value": [
            "$__all"
          ]
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
        "hide": 2,
        "includeAll": true,
        "label": "pod",
        "multi": true,
        "name": "pod",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Controller-Runtime-Metrics",
  "weekStart": ""
}


================================================
FILE: testdata/project-v4-multigroup/grafana/custom-metrics/config.yaml
================================================
---
customMetrics:
#  - metric: # Raw custom metric (required)
#    type:   # Metric type: counter/gauge/histogram (required)
#    expr:   # Prom_ql for the metric (optional)
#    unit:   # Unit of measurement, examples: s,none,bytes,percent,etc. (optional)
#
#
# Example:
# ---
# customMetrics:
#   - metric: foo_bar
#     unit: none
#     type: histogram
#   	expr: histogram_quantile(0.90, sum by(instance, le) (rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m])))


================================================
FILE: testdata/project-v4-multigroup/hack/boilerplate.go.txt
================================================
/*
Copyright 2026 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: testdata/project-v4-multigroup/internal/controller/apps/deployment_controller.go
================================================
/*
Copyright 2026 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 apps

import (
	"context"

	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// DeploymentReconciler reconciles a Deployment object
type DeploymentReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Deployment object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&appsv1.Deployment{}).
		Named("apps-deployment").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/apps/deployment_controller_test.go
================================================
/*
Copyright 2026 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 apps

import (
	. "github.com/onsi/ginkgo/v2"
)

var _ = Describe("Deployment Controller", func() {
	Context("When reconciling a resource", func() {

		It("should successfully reconcile the resource", func() {

			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/apps/suite_test.go
================================================
/*
Copyright 2026 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 apps

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = appsv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/cert-manager/certificate_controller.go
================================================
/*
Copyright 2026 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 certmanager

import (
	"context"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// CertificateReconciler reconciles a Certificate object
type CertificateReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Certificate object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *CertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *CertificateReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&certmanagerv1.Certificate{}).
		Named("cert-manager-certificate").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/cert-manager/certificate_controller_test.go
================================================
/*
Copyright 2026 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 certmanager

import (
	. "github.com/onsi/ginkgo/v2"
)

var _ = Describe("Certificate Controller", func() {
	Context("When reconciling a resource", func() {

		It("should successfully reconcile the resource", func() {

			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/cert-manager/suite_test.go
================================================
/*
Copyright 2026 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 certmanager

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = certmanagerv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/crew/captain_controller.go
================================================
/*
Copyright 2026 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 crew

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1"
)

// CaptainReconciler reconciles a Captain object
type CaptainReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=crew.testproject.org,resources=captains,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=crew.testproject.org,resources=captains/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=crew.testproject.org,resources=captains/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Captain object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *CaptainReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&crewv1.Captain{}).
		Named("crew-captain").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/crew/captain_controller_test.go
================================================
/*
Copyright 2026 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 crew

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1"
)

var _ = Describe("Captain Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		captain := &crewv1.Captain{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Captain")
			err := k8sClient.Get(ctx, typeNamespacedName, captain)
			if err != nil && errors.IsNotFound(err) {
				resource := &crewv1.Captain{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &crewv1.Captain{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Captain")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &CaptainReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/crew/suite_test.go
================================================
/*
Copyright 2026 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 crew

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = crewv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller.go
================================================
/*
Copyright 2026 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 examplecom

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	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/types"
	"k8s.io/client-go/tools/events"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
)

const busyboxFinalizer = "example.com.testproject.org/finalizer"

// Definitions to manage status conditions
const (
	// typeAvailableBusybox represents the status of the Deployment reconciliation
	typeAvailableBusybox = "Available"
	// typeDegradedBusybox represents the status used when the custom resource is deleted and the finalizer operations are yet to occur.
	typeDegradedBusybox = "Degraded"
)

// BusyboxReconciler reconciles a Busybox object
type BusyboxReconciler struct {
	client.Client
	Scheme   *runtime.Scheme
	Recorder events.EventRecorder
}

// The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen
// when the command  is executed.
// To know more about markers see: https://book.kubebuilder.io/reference/markers.html

// +kubebuilder:rbac:groups=example.com.testproject.org,resources=busyboxes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=example.com.testproject.org,resources=busyboxes/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=example.com.testproject.org,resources=busyboxes/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator
// pattern you will create Controllers which provide a reconcile function
// responsible for synchronizing resources until the desired state is reached on the cluster.
// Breaking this recommendation goes against the design principles of controller-runtime.
// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention.
// For further info:
// - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
// - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := logf.FromContext(ctx)

	// Fetch the Busybox instance
	// The purpose is check if the Custom Resource for the Kind Busybox
	// is applied on the cluster if not we return nil to stop the reconciliation
	busybox := &examplecomv1alpha1.Busybox{}
	err := r.Get(ctx, req.NamespacedName, busybox)
	if err != nil {
		if apierrors.IsNotFound(err) {
			// If the custom resource is not found then it usually means that it was deleted or not created
			// In this way, we will stop the reconciliation
			log.Info("Busybox resource not found, ignoring since object must be deleted")
			return ctrl.Result{}, nil
		}
		// Error reading the object - requeue the request.
		log.Error(err, "Failed to get busybox")
		return ctrl.Result{}, err
	}

	if len(busybox.Status.Conditions) == 0 {
		meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"})
		if err = r.Status().Update(ctx, busybox); err != nil {
			log.Error(err, "Failed to update Busybox status")
			return ctrl.Result{}, err
		}

		// Let's re-fetch the busybox Custom Resource after updating the status
		// so that we have the latest state of the resource on the cluster and we will avoid
		// raising the error "the object has been modified, please apply
		// your changes to the latest version and try again" which would re-trigger the reconciliation
		// if we try to update it again in the following operations
		if err := r.Get(ctx, req.NamespacedName, busybox); err != nil {
			log.Error(err, "Failed to re-fetch busybox")
			return ctrl.Result{}, err
		}
	}

	// Let's add a finalizer. Then, we can define some operations which should
	// occur before the custom resource is deleted.
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers
	if !controllerutil.ContainsFinalizer(busybox, busyboxFinalizer) {
		log.Info("Adding finalizer for Busybox")
		controllerutil.AddFinalizer(busybox, busyboxFinalizer)
		if err = r.Update(ctx, busybox); err != nil {
			log.Error(err, "Failed to update custom resource to add finalizer")
			return ctrl.Result{}, err
		}
	}

	// Check if the Busybox instance is marked to be deleted, which is
	// indicated by the deletion timestamp being set.
	isBusyboxMarkedToBeDeleted := busybox.GetDeletionTimestamp() != nil
	if isBusyboxMarkedToBeDeleted {
		if controllerutil.ContainsFinalizer(busybox, busyboxFinalizer) {
			log.Info("Performing finalizer operations for Busybox before deleting CR")

			// Let's add here a status "Downgrade" to reflect that this resource began its process to be terminated.
			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeDegradedBusybox,
				Status: metav1.ConditionUnknown, Reason: "Finalizing",
				Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", busybox.Name)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			// Perform all operations required before removing the finalizer and allow
			// the Kubernetes API to remove the custom resource.
			r.doFinalizerOperationsForBusybox(busybox)

			// TODO(user): If you add operations to the doFinalizerOperationsForBusybox method
			// then you need to ensure that all worked fine before deleting and updating the Downgrade status
			// otherwise, you should requeue here.

			// Re-fetch the busybox Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, busybox); err != nil {
				log.Error(err, "Failed to re-fetch busybox")
				return ctrl.Result{}, err
			}

			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeDegradedBusybox,
				Status: metav1.ConditionTrue, Reason: "Finalizing",
				Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", busybox.Name)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			log.Info("Removing finalizer for Busybox after successfully performing the operations")
			if ok := controllerutil.RemoveFinalizer(busybox, busyboxFinalizer); !ok {
				err = fmt.Errorf("finalizer for Busybox was not removed")
				log.Error(err, "Failed to remove finalizer for Busybox")
				return ctrl.Result{}, err
			}

			if err := r.Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to remove finalizer for Busybox")
				return ctrl.Result{}, err
			}
		}
		return ctrl.Result{}, nil
	}

	// Check if the deployment already exists, if not create a new one
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: busybox.Name, Namespace: busybox.Namespace}, found)
	if err != nil && apierrors.IsNotFound(err) {
		// Define a new deployment
		dep, err := r.deploymentForBusybox(busybox)
		if err != nil {
			log.Error(err, "Failed to define new Deployment resource for Busybox")

			// The following implementation will update the status
			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox,
				Status: metav1.ConditionFalse, Reason: "Reconciling",
				Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", busybox.Name, err)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		log.Info("Creating a new Deployment",
			"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		if err = r.Create(ctx, dep); err != nil {
			log.Error(err, "Failed to create new Deployment",
				"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}

		// Deployment created successfully
		// We will requeue the reconciliation so that we can ensure the state
		// and move forward for the next operations
		return ctrl.Result{RequeueAfter: time.Minute}, nil
	} else if err != nil {
		log.Error(err, "Failed to get Deployment")
		// Let's return the error for the reconciliation be re-triggered again
		return ctrl.Result{}, err
	}

	// If the size is not defined in the Custom Resource then we will set the desired replicas to 0
	var desiredReplicas int32 = 0
	if busybox.Spec.Size != nil {
		desiredReplicas = *busybox.Spec.Size
	}

	// The CRD API defines that the Busybox type have a BusyboxSpec.Size field
	// to set the quantity of Deployment instances to the desired state on the cluster.
	// Therefore, the following code will ensure the Deployment size is the same as defined
	// via the Size spec of the Custom Resource which we are reconciling.
	if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas {
		found.Spec.Replicas = ptr.To(desiredReplicas)
		if err = r.Update(ctx, found); err != nil {
			log.Error(err, "Failed to update Deployment",
				"Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)

			// Re-fetch the busybox Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, busybox); err != nil {
				log.Error(err, "Failed to re-fetch busybox")
				return ctrl.Result{}, err
			}

			// The following implementation will update the status
			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox,
				Status: metav1.ConditionFalse, Reason: "Resizing",
				Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", busybox.Name, err)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		// Now, that we update the size we want to requeue the reconciliation
		// so that we can ensure that we have the latest state of the resource before
		// update. Also, it will help ensure the desired state on the cluster
		return ctrl.Result{Requeue: true}, nil
	}

	// The following implementation will update the status
	meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox,
		Status: metav1.ConditionTrue, Reason: "Reconciling",
		Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", busybox.Name, desiredReplicas)})

	if err := r.Status().Update(ctx, busybox); err != nil {
		log.Error(err, "Failed to update Busybox status")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

// finalizeBusybox will perform the required operations before delete the CR.
func (r *BusyboxReconciler) doFinalizerOperationsForBusybox(cr *examplecomv1alpha1.Busybox) {
	// TODO(user): Add the cleanup steps that the operator
	// needs to do before the CR can be deleted. Examples
	// of finalizers include performing backups and deleting
	// resources that are not owned by this CR, like a PVC.

	// Note: It is not recommended to use finalizers with the purpose of deleting resources which are
	// created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile,
	// are defined as dependent of the custom resource. See that we use the method ctrl.SetControllerReference.
	// to set the ownerRef which means that the Deployment will be deleted by the Kubernetes API.
	// More info: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/

	// The following implementation will raise an event
	r.Recorder.Eventf(cr, nil, corev1.EventTypeWarning, "Deleting", "DeleteCR",
		"Custom Resource %s is being deleted from the namespace %s",
		cr.Name,
		cr.Namespace)
}

// deploymentForBusybox returns a Busybox Deployment object
func (r *BusyboxReconciler) deploymentForBusybox(
	busybox *examplecomv1alpha1.Busybox) (*appsv1.Deployment, error) {
	ls := labelsForBusybox()

	// Get the Operand image
	image, err := imageForBusybox()
	if err != nil {
		return nil, err
	}

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      busybox.Name,
			Namespace: busybox.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: busybox.Spec.Size,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: corev1.PodSpec{
					// TODO(user): Uncomment the following code to configure the nodeAffinity expression
					// according to the platforms which are supported by your solution. It is considered
					// best practice to support multiple architectures. build your manager image using the
					// makefile target docker-buildx. Also, you can use docker manifest inspect 
					// to check what are the platforms supported.
					// More info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
					// Affinity: &corev1.Affinity{
					//	 NodeAffinity: &corev1.NodeAffinity{
					//		 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
					//			 NodeSelectorTerms: []corev1.NodeSelectorTerm{
					//				 {
					//					 MatchExpressions: []corev1.NodeSelectorRequirement{
					//						 {
					//							 Key:      "kubernetes.io/arch",
					//							 Operator: "In",
					//							 Values:   []string{"amd64", "arm64", "ppc64le", "s390x"},
					//						 },
					//						 {
					//							 Key:      "kubernetes.io/os",
					//							 Operator: "In",
					//							 Values:   []string{"linux"},
					//						 },
					//					 },
					//				 },
					//		 	 },
					//		 },
					//	 },
					// },
					SecurityContext: &corev1.PodSecurityContext{
						RunAsNonRoot: ptr.To(true),
						// IMPORTANT: seccomProfile was introduced with Kubernetes 1.19
						// If you are looking for to produce solutions to be supported
						// on lower versions you must remove this option.
						SeccompProfile: &corev1.SeccompProfile{
							Type: corev1.SeccompProfileTypeRuntimeDefault,
						},
					},
					Containers: []corev1.Container{{
						Image:           image,
						Name:            "busybox",
						ImagePullPolicy: corev1.PullIfNotPresent,
						// Ensure restrictive context for the container
						// More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
						SecurityContext: &corev1.SecurityContext{
							RunAsNonRoot:             ptr.To(true),
							AllowPrivilegeEscalation: ptr.To(false),
							Capabilities: &corev1.Capabilities{
								Drop: []corev1.Capability{
									"ALL",
								},
							},
						},
					}},
				},
			},
		},
	}

	// Set the ownerRef for the Deployment
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/
	if err := ctrl.SetControllerReference(busybox, dep, r.Scheme); err != nil {
		return nil, err
	}
	return dep, nil
}

// labelsForBusybox returns the labels for selecting the resources
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
func labelsForBusybox() map[string]string {
	var imageTag string
	image, err := imageForBusybox()
	if err == nil {
		imageTag = strings.Split(image, ":")[1]
	}
	return map[string]string{
		"app.kubernetes.io/name":       "project-v4-multigroup",
		"app.kubernetes.io/version":    imageTag,
		"app.kubernetes.io/managed-by": "BusyboxController",
	}
}

// imageForBusybox gets the Operand image which is managed by this controller
// from the BUSYBOX_IMAGE environment variable defined in the config/manager/manager.yaml
func imageForBusybox() (string, error) {
	var imageEnvVar = "BUSYBOX_IMAGE"
	image, found := os.LookupEnv(imageEnvVar)
	if !found {
		return "", fmt.Errorf("unable to find %s environment variable with the image", imageEnvVar)
	}
	return image, nil
}

// SetupWithManager sets up the controller with the Manager.
// The whole idea is to be watching the resources that matter for the controller.
// When a resource that the controller is interested in changes, the Watch triggers
// the controller’s reconciliation loop, ensuring that the actual state of the resource
// matches the desired state as defined in the controller’s logic.
//
// Notice how we configured the Manager to monitor events such as the creation, update,
// or deletion of a Custom Resource (CR) of the Busybox kind, as well as any changes
// to the Deployment that the controller manages and owns.
func (r *BusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		// Watch the Busybox CR(s) and trigger reconciliation whenever it
		// is created, updated, or deleted
		For(&examplecomv1alpha1.Busybox{}).
		Named("example.com-busybox").
		// Watch the Deployment managed by the BusyboxReconciler. If any changes occur to the Deployment
		// owned and managed by this controller, it will trigger reconciliation, ensuring that the cluster
		// state aligns with the desired state. See that the ownerRef was set when the Deployment was created.
		Owns(&appsv1.Deployment{}).
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/example.com/busybox_controller_test.go
================================================
/*
Copyright 2026 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 examplecom

import (
	"context"
	"os"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
)

var _ = Describe("Busybox controller", func() {
	Context("Busybox controller test", func() {

		const BusyboxName = "test-busybox"

		ctx := context.Background()

		namespace := &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name:      BusyboxName,
				Namespace: BusyboxName,
			},
		}

		typeNamespacedName := types.NamespacedName{
			Name:      BusyboxName,
			Namespace: BusyboxName,
		}
		busybox := &examplecomv1alpha1.Busybox{}

		SetDefaultEventuallyTimeout(2 * time.Minute)
		SetDefaultEventuallyPollingInterval(time.Second)

		BeforeEach(func() {
			By("Creating the Namespace to perform the tests")
			err := k8sClient.Create(ctx, namespace)
			Expect(err).NotTo(HaveOccurred())

			By("Setting the Image ENV VAR which stores the Operand image")
			err = os.Setenv("BUSYBOX_IMAGE", "example.com/image:test")
			Expect(err).NotTo(HaveOccurred())

			By("creating the custom resource for the Kind Busybox")
			err = k8sClient.Get(ctx, typeNamespacedName, busybox)
			if err != nil && errors.IsNotFound(err) {
				// Let's mock our custom resource at the same way that we would
				// apply on the cluster the manifest under config/samples
				busybox = &examplecomv1alpha1.Busybox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      BusyboxName,
						Namespace: namespace.Name,
					},
					Spec: examplecomv1alpha1.BusyboxSpec{
						Size: ptr.To(int32(1)),
					},
				}

				err = k8sClient.Create(ctx, busybox)
				Expect(err).NotTo(HaveOccurred())
			}
		})

		AfterEach(func() {
			By("removing the custom resource for the Kind Busybox")
			found := &examplecomv1alpha1.Busybox{}
			err := k8sClient.Get(ctx, typeNamespacedName, found)
			Expect(err).NotTo(HaveOccurred())

			Eventually(func(g Gomega) {
				g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed())
			}).Should(Succeed())

			// TODO(user): Attention if you improve this code by adding other context test you MUST
			// be aware of the current delete namespace limitations.
			// More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations
			By("Deleting the Namespace to perform the tests")
			_ = k8sClient.Delete(ctx, namespace)

			By("Removing the Image ENV VAR which stores the Operand image")
			_ = os.Unsetenv("BUSYBOX_IMAGE")
		})

		It("should successfully reconcile a custom resource for Busybox", func() {
			By("Checking if the custom resource was successfully created")
			Eventually(func(g Gomega) {
				found := &examplecomv1alpha1.Busybox{}
				Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource created")
			busyboxReconciler := &BusyboxReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := busyboxReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking if Deployment was successfully created in the reconciliation")
			Eventually(func(g Gomega) {
				found := &appsv1.Deployment{}
				g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource again")
			_, err = busyboxReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking the latest Status Condition added to the Busybox instance")
			Expect(k8sClient.Get(ctx, typeNamespacedName, busybox)).To(Succeed())
			var conditions []metav1.Condition
			Expect(busybox.Status.Conditions).To(ContainElement(
				HaveField("Type", Equal(typeAvailableBusybox)), &conditions))
			Expect(conditions).To(HaveLen(1), "Multiple conditions of type %s", typeAvailableBusybox)
			Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "condition %s", typeAvailableBusybox)
			Expect(conditions[0].Reason).To(Equal("Reconciling"), "condition %s", typeAvailableBusybox)
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller.go
================================================
/*
Copyright 2026 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 examplecom

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	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/types"
	"k8s.io/client-go/tools/events"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
)

const memcachedFinalizer = "example.com.testproject.org/finalizer"

// Definitions to manage status conditions
const (
	// typeAvailableMemcached represents the status of the Deployment reconciliation
	typeAvailableMemcached = "Available"
	// typeDegradedMemcached represents the status used when the custom resource is deleted and the finalizer operations are yet to occur.
	typeDegradedMemcached = "Degraded"
)

// MemcachedReconciler reconciles a Memcached object
type MemcachedReconciler struct {
	client.Client
	Scheme   *runtime.Scheme
	Recorder events.EventRecorder
}

// The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen
// when the command  is executed.
// To know more about markers see: https://book.kubebuilder.io/reference/markers.html

// +kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=example.com.testproject.org,resources=memcacheds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator
// pattern you will create Controllers which provide a reconcile function
// responsible for synchronizing resources until the desired state is reached on the cluster.
// Breaking this recommendation goes against the design principles of controller-runtime.
// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention.
// For further info:
// - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
// - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := logf.FromContext(ctx)

	// Fetch the Memcached instance
	// The purpose is check if the Custom Resource for the Kind Memcached
	// is applied on the cluster if not we return nil to stop the reconciliation
	memcached := &examplecomv1alpha1.Memcached{}
	err := r.Get(ctx, req.NamespacedName, memcached)
	if err != nil {
		if apierrors.IsNotFound(err) {
			// If the custom resource is not found then it usually means that it was deleted or not created
			// In this way, we will stop the reconciliation
			log.Info("Memcached resource not found, ignoring since object must be deleted")
			return ctrl.Result{}, nil
		}
		// Error reading the object - requeue the request.
		log.Error(err, "Failed to get memcached")
		return ctrl.Result{}, err
	}

	if len(memcached.Status.Conditions) == 0 {
		meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"})
		if err = r.Status().Update(ctx, memcached); err != nil {
			log.Error(err, "Failed to update Memcached status")
			return ctrl.Result{}, err
		}

		// Let's re-fetch the memcached Custom Resource after updating the status
		// so that we have the latest state of the resource on the cluster and we will avoid
		// raising the error "the object has been modified, please apply
		// your changes to the latest version and try again" which would re-trigger the reconciliation
		// if we try to update it again in the following operations
		if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
			log.Error(err, "Failed to re-fetch memcached")
			return ctrl.Result{}, err
		}
	}

	// Let's add a finalizer. Then, we can define some operations which should
	// occur before the custom resource is deleted.
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers
	if !controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) {
		log.Info("Adding finalizer for Memcached")
		controllerutil.AddFinalizer(memcached, memcachedFinalizer)
		if err = r.Update(ctx, memcached); err != nil {
			log.Error(err, "Failed to update custom resource to add finalizer")
			return ctrl.Result{}, err
		}
	}

	// Check if the Memcached instance is marked to be deleted, which is
	// indicated by the deletion timestamp being set.
	isMemcachedMarkedToBeDeleted := memcached.GetDeletionTimestamp() != nil
	if isMemcachedMarkedToBeDeleted {
		if controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) {
			log.Info("Performing finalizer operations for Memcached before deleting CR")

			// Let's add here a status "Downgrade" to reflect that this resource began its process to be terminated.
			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeDegradedMemcached,
				Status: metav1.ConditionUnknown, Reason: "Finalizing",
				Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", memcached.Name)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			// Perform all operations required before removing the finalizer and allow
			// the Kubernetes API to remove the custom resource.
			r.doFinalizerOperationsForMemcached(memcached)

			// TODO(user): If you add operations to the doFinalizerOperationsForMemcached method
			// then you need to ensure that all worked fine before deleting and updating the Downgrade status
			// otherwise, you should requeue here.

			// Re-fetch the memcached Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
				log.Error(err, "Failed to re-fetch memcached")
				return ctrl.Result{}, err
			}

			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeDegradedMemcached,
				Status: metav1.ConditionTrue, Reason: "Finalizing",
				Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", memcached.Name)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			log.Info("Removing finalizer for Memcached after successfully performing the operations")
			if ok := controllerutil.RemoveFinalizer(memcached, memcachedFinalizer); !ok {
				err = fmt.Errorf("finalizer for Memcached was not removed")
				log.Error(err, "Failed to remove finalizer for Memcached")
				return ctrl.Result{}, err
			}

			if err := r.Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to remove finalizer for Memcached")
				return ctrl.Result{}, err
			}
		}
		return ctrl.Result{}, nil
	}

	// Check if the deployment already exists, if not create a new one
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found)
	if err != nil && apierrors.IsNotFound(err) {
		// Define a new deployment
		dep, err := r.deploymentForMemcached(memcached)
		if err != nil {
			log.Error(err, "Failed to define new Deployment resource for Memcached")

			// The following implementation will update the status
			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
				Status: metav1.ConditionFalse, Reason: "Reconciling",
				Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", memcached.Name, err)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		log.Info("Creating a new Deployment",
			"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		if err = r.Create(ctx, dep); err != nil {
			log.Error(err, "Failed to create new Deployment",
				"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}

		// Deployment created successfully
		// We will requeue the reconciliation so that we can ensure the state
		// and move forward for the next operations
		return ctrl.Result{RequeueAfter: time.Minute}, nil
	} else if err != nil {
		log.Error(err, "Failed to get Deployment")
		// Let's return the error for the reconciliation be re-triggered again
		return ctrl.Result{}, err
	}

	// If the size is not defined in the Custom Resource then we will set the desired replicas to 0
	var desiredReplicas int32 = 0
	if memcached.Spec.Size != nil {
		desiredReplicas = *memcached.Spec.Size
	}

	// The CRD API defines that the Memcached type have a MemcachedSpec.Size field
	// to set the quantity of Deployment instances to the desired state on the cluster.
	// Therefore, the following code will ensure the Deployment size is the same as defined
	// via the Size spec of the Custom Resource which we are reconciling.
	if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas {
		found.Spec.Replicas = ptr.To(desiredReplicas)
		if err = r.Update(ctx, found); err != nil {
			log.Error(err, "Failed to update Deployment",
				"Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)

			// Re-fetch the memcached Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
				log.Error(err, "Failed to re-fetch memcached")
				return ctrl.Result{}, err
			}

			// The following implementation will update the status
			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
				Status: metav1.ConditionFalse, Reason: "Resizing",
				Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", memcached.Name, err)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		// Now, that we update the size we want to requeue the reconciliation
		// so that we can ensure that we have the latest state of the resource before
		// update. Also, it will help ensure the desired state on the cluster
		return ctrl.Result{Requeue: true}, nil
	}

	// The following implementation will update the status
	meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
		Status: metav1.ConditionTrue, Reason: "Reconciling",
		Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", memcached.Name, desiredReplicas)})

	if err := r.Status().Update(ctx, memcached); err != nil {
		log.Error(err, "Failed to update Memcached status")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

// finalizeMemcached will perform the required operations before delete the CR.
func (r *MemcachedReconciler) doFinalizerOperationsForMemcached(cr *examplecomv1alpha1.Memcached) {
	// TODO(user): Add the cleanup steps that the operator
	// needs to do before the CR can be deleted. Examples
	// of finalizers include performing backups and deleting
	// resources that are not owned by this CR, like a PVC.

	// Note: It is not recommended to use finalizers with the purpose of deleting resources which are
	// created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile,
	// are defined as dependent of the custom resource. See that we use the method ctrl.SetControllerReference.
	// to set the ownerRef which means that the Deployment will be deleted by the Kubernetes API.
	// More info: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/

	// The following implementation will raise an event
	r.Recorder.Eventf(cr, nil, corev1.EventTypeWarning, "Deleting", "DeleteCR",
		"Custom Resource %s is being deleted from the namespace %s",
		cr.Name,
		cr.Namespace)
}

// deploymentForMemcached returns a Memcached Deployment object
func (r *MemcachedReconciler) deploymentForMemcached(
	memcached *examplecomv1alpha1.Memcached) (*appsv1.Deployment, error) {
	ls := labelsForMemcached()

	// Get the Operand image
	image, err := imageForMemcached()
	if err != nil {
		return nil, err
	}

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      memcached.Name,
			Namespace: memcached.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: memcached.Spec.Size,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: corev1.PodSpec{
					// TODO(user): Uncomment the following code to configure the nodeAffinity expression
					// according to the platforms which are supported by your solution. It is considered
					// best practice to support multiple architectures. build your manager image using the
					// makefile target docker-buildx. Also, you can use docker manifest inspect 
					// to check what are the platforms supported.
					// More info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
					// Affinity: &corev1.Affinity{
					//	 NodeAffinity: &corev1.NodeAffinity{
					//		 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
					//			 NodeSelectorTerms: []corev1.NodeSelectorTerm{
					//				 {
					//					 MatchExpressions: []corev1.NodeSelectorRequirement{
					//						 {
					//							 Key:      "kubernetes.io/arch",
					//							 Operator: "In",
					//							 Values:   []string{"amd64", "arm64", "ppc64le", "s390x"},
					//						 },
					//						 {
					//							 Key:      "kubernetes.io/os",
					//							 Operator: "In",
					//							 Values:   []string{"linux"},
					//						 },
					//					 },
					//				 },
					//		 	 },
					//		 },
					//	 },
					// },
					SecurityContext: &corev1.PodSecurityContext{
						RunAsNonRoot: ptr.To(true),
						// IMPORTANT: seccomProfile was introduced with Kubernetes 1.19
						// If you are looking for to produce solutions to be supported
						// on lower versions you must remove this option.
						SeccompProfile: &corev1.SeccompProfile{
							Type: corev1.SeccompProfileTypeRuntimeDefault,
						},
					},
					Containers: []corev1.Container{{
						Image:           image,
						Name:            "memcached",
						ImagePullPolicy: corev1.PullIfNotPresent,
						// Ensure restrictive context for the container
						// More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
						SecurityContext: &corev1.SecurityContext{
							RunAsNonRoot:             ptr.To(true),
							RunAsUser:                ptr.To(int64(1001)),
							AllowPrivilegeEscalation: ptr.To(false),
							Capabilities: &corev1.Capabilities{
								Drop: []corev1.Capability{
									"ALL",
								},
							},
						},
						Ports: []corev1.ContainerPort{{
							ContainerPort: memcached.Spec.ContainerPort,
							Name:          "memcached",
						}},
						Command: []string{"memcached", "--memory-limit=64", "-o", "modern", "-v"},
					}},
				},
			},
		},
	}

	// Set the ownerRef for the Deployment
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/
	if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil {
		return nil, err
	}
	return dep, nil
}

// labelsForMemcached returns the labels for selecting the resources
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
func labelsForMemcached() map[string]string {
	var imageTag string
	image, err := imageForMemcached()
	if err == nil {
		imageTag = strings.Split(image, ":")[1]
	}
	return map[string]string{
		"app.kubernetes.io/name":       "project-v4-multigroup",
		"app.kubernetes.io/version":    imageTag,
		"app.kubernetes.io/managed-by": "MemcachedController",
	}
}

// imageForMemcached gets the Operand image which is managed by this controller
// from the MEMCACHED_IMAGE environment variable defined in the config/manager/manager.yaml
func imageForMemcached() (string, error) {
	var imageEnvVar = "MEMCACHED_IMAGE"
	image, found := os.LookupEnv(imageEnvVar)
	if !found {
		return "", fmt.Errorf("unable to find %s environment variable with the image", imageEnvVar)
	}
	return image, nil
}

// SetupWithManager sets up the controller with the Manager.
// The whole idea is to be watching the resources that matter for the controller.
// When a resource that the controller is interested in changes, the Watch triggers
// the controller’s reconciliation loop, ensuring that the actual state of the resource
// matches the desired state as defined in the controller’s logic.
//
// Notice how we configured the Manager to monitor events such as the creation, update,
// or deletion of a Custom Resource (CR) of the Memcached kind, as well as any changes
// to the Deployment that the controller manages and owns.
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		// Watch the Memcached CR(s) and trigger reconciliation whenever it
		// is created, updated, or deleted
		For(&examplecomv1alpha1.Memcached{}).
		Named("example.com-memcached").
		// Watch the Deployment managed by the MemcachedReconciler. If any changes occur to the Deployment
		// owned and managed by this controller, it will trigger reconciliation, ensuring that the cluster
		// state aligns with the desired state. See that the ownerRef was set when the Deployment was created.
		Owns(&appsv1.Deployment{}).
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/example.com/memcached_controller_test.go
================================================
/*
Copyright 2026 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 examplecom

import (
	"context"
	"os"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
)

var _ = Describe("Memcached controller", func() {
	Context("Memcached controller test", func() {

		const MemcachedName = "test-memcached"

		ctx := context.Background()

		namespace := &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name:      MemcachedName,
				Namespace: MemcachedName,
			},
		}

		typeNamespacedName := types.NamespacedName{
			Name:      MemcachedName,
			Namespace: MemcachedName,
		}
		memcached := &examplecomv1alpha1.Memcached{}

		SetDefaultEventuallyTimeout(2 * time.Minute)
		SetDefaultEventuallyPollingInterval(time.Second)

		BeforeEach(func() {
			By("Creating the Namespace to perform the tests")
			err := k8sClient.Create(ctx, namespace)
			Expect(err).NotTo(HaveOccurred())

			By("Setting the Image ENV VAR which stores the Operand image")
			err = os.Setenv("MEMCACHED_IMAGE", "example.com/image:test")
			Expect(err).NotTo(HaveOccurred())

			By("creating the custom resource for the Kind Memcached")
			err = k8sClient.Get(ctx, typeNamespacedName, memcached)
			if err != nil && errors.IsNotFound(err) {
				// Let's mock our custom resource at the same way that we would
				// apply on the cluster the manifest under config/samples
				memcached = &examplecomv1alpha1.Memcached{
					ObjectMeta: metav1.ObjectMeta{
						Name:      MemcachedName,
						Namespace: namespace.Name,
					},
					Spec: examplecomv1alpha1.MemcachedSpec{
						Size:          ptr.To(int32(1)),
						ContainerPort: 11211,
					},
				}

				err = k8sClient.Create(ctx, memcached)
				Expect(err).NotTo(HaveOccurred())
			}
		})

		AfterEach(func() {
			By("removing the custom resource for the Kind Memcached")
			found := &examplecomv1alpha1.Memcached{}
			err := k8sClient.Get(ctx, typeNamespacedName, found)
			Expect(err).NotTo(HaveOccurred())

			Eventually(func(g Gomega) {
				g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed())
			}).Should(Succeed())

			// TODO(user): Attention if you improve this code by adding other context test you MUST
			// be aware of the current delete namespace limitations.
			// More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations
			By("Deleting the Namespace to perform the tests")
			_ = k8sClient.Delete(ctx, namespace)

			By("Removing the Image ENV VAR which stores the Operand image")
			_ = os.Unsetenv("MEMCACHED_IMAGE")
		})

		It("should successfully reconcile a custom resource for Memcached", func() {
			By("Checking if the custom resource was successfully created")
			Eventually(func(g Gomega) {
				found := &examplecomv1alpha1.Memcached{}
				Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource created")
			memcachedReconciler := &MemcachedReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := memcachedReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking if Deployment was successfully created in the reconciliation")
			Eventually(func(g Gomega) {
				found := &appsv1.Deployment{}
				g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource again")
			_, err = memcachedReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking the latest Status Condition added to the Memcached instance")
			Expect(k8sClient.Get(ctx, typeNamespacedName, memcached)).To(Succeed())
			var conditions []metav1.Condition
			Expect(memcached.Status.Conditions).To(ContainElement(
				HaveField("Type", Equal(typeAvailableMemcached)), &conditions))
			Expect(conditions).To(HaveLen(1), "Multiple conditions of type %s", typeAvailableMemcached)
			Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "condition %s", typeAvailableMemcached)
			Expect(conditions[0].Reason).To(Equal("Reconciling"), "condition %s", typeAvailableMemcached)
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/example.com/suite_test.go
================================================
/*
Copyright 2026 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 examplecom

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = examplecomv1alpha1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	err = examplecomv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/example.com/wordpress_controller.go
================================================
/*
Copyright 2026 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 examplecom

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
)

// WordpressReconciler reconciles a Wordpress object
type WordpressReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=example.com.testproject.org,resources=wordpresses,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=example.com.testproject.org,resources=wordpresses/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=example.com.testproject.org,resources=wordpresses/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Wordpress object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *WordpressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *WordpressReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&examplecomv1.Wordpress{}).
		Named("example.com-wordpress").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/example.com/wordpress_controller_test.go
================================================
/*
Copyright 2026 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 examplecom

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
)

var _ = Describe("Wordpress Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		wordpress := &examplecomv1.Wordpress{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Wordpress")
			err := k8sClient.Get(ctx, typeNamespacedName, wordpress)
			if err != nil && errors.IsNotFound(err) {
				resource := &examplecomv1.Wordpress{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &examplecomv1.Wordpress{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Wordpress")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &WordpressReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/fiz/bar_controller.go
================================================
/*
Copyright 2026 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 fiz

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1"
)

// BarReconciler reconciles a Bar object
type BarReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=fiz.testproject.org,resources=bars,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=fiz.testproject.org,resources=bars/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=fiz.testproject.org,resources=bars/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Bar object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *BarReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *BarReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&fizv1.Bar{}).
		Named("fiz-bar").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/fiz/bar_controller_test.go
================================================
/*
Copyright 2026 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 fiz

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1"
)

var _ = Describe("Bar Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		bar := &fizv1.Bar{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Bar")
			err := k8sClient.Get(ctx, typeNamespacedName, bar)
			if err != nil && errors.IsNotFound(err) {
				resource := &fizv1.Bar{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &fizv1.Bar{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Bar")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &BarReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/fiz/suite_test.go
================================================
/*
Copyright 2026 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 fiz

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	fizv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/fiz/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = fizv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/foo/bar_controller.go
================================================
/*
Copyright 2026 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 foo

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo/v1"
)

// BarReconciler reconciles a Bar object
type BarReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=foo.testproject.org,resources=bars,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=foo.testproject.org,resources=bars/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=foo.testproject.org,resources=bars/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Bar object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *BarReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *BarReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&foov1.Bar{}).
		Named("foo-bar").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/foo/bar_controller_test.go
================================================
/*
Copyright 2026 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 foo

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo/v1"
)

var _ = Describe("Bar Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		bar := &foov1.Bar{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Bar")
			err := k8sClient.Get(ctx, typeNamespacedName, bar)
			if err != nil && errors.IsNotFound(err) {
				resource := &foov1.Bar{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &foov1.Bar{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Bar")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &BarReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/foo/suite_test.go
================================================
/*
Copyright 2026 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 foo

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	foov1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = foov1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/foo.policy/healthcheckpolicy_controller.go
================================================
/*
Copyright 2026 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 foopolicy

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	foopolicyv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo.policy/v1"
)

// HealthCheckPolicyReconciler reconciles a HealthCheckPolicy object
type HealthCheckPolicyReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=foo.policy.testproject.org,resources=healthcheckpolicies,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=foo.policy.testproject.org,resources=healthcheckpolicies/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=foo.policy.testproject.org,resources=healthcheckpolicies/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the HealthCheckPolicy object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *HealthCheckPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *HealthCheckPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&foopolicyv1.HealthCheckPolicy{}).
		Named("foo.policy-healthcheckpolicy").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/foo.policy/healthcheckpolicy_controller_test.go
================================================
/*
Copyright 2026 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 foopolicy

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	foopolicyv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo.policy/v1"
)

var _ = Describe("HealthCheckPolicy Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		healthcheckpolicy := &foopolicyv1.HealthCheckPolicy{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind HealthCheckPolicy")
			err := k8sClient.Get(ctx, typeNamespacedName, healthcheckpolicy)
			if err != nil && errors.IsNotFound(err) {
				resource := &foopolicyv1.HealthCheckPolicy{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &foopolicyv1.HealthCheckPolicy{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance HealthCheckPolicy")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &HealthCheckPolicyReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/foo.policy/suite_test.go
================================================
/*
Copyright 2026 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 foopolicy

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	foopolicyv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/foo.policy/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = foopolicyv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/sea-creatures/kraken_controller.go
================================================
/*
Copyright 2026 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 seacreatures

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	seacreaturesv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta1"
)

// KrakenReconciler reconciles a Kraken object
type KrakenReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=krakens,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=krakens/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=krakens/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Kraken object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *KrakenReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *KrakenReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&seacreaturesv1beta1.Kraken{}).
		Named("sea-creatures-kraken").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/sea-creatures/kraken_controller_test.go
================================================
/*
Copyright 2026 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 seacreatures

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	seacreaturesv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta1"
)

var _ = Describe("Kraken Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		kraken := &seacreaturesv1beta1.Kraken{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Kraken")
			err := k8sClient.Get(ctx, typeNamespacedName, kraken)
			if err != nil && errors.IsNotFound(err) {
				resource := &seacreaturesv1beta1.Kraken{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &seacreaturesv1beta1.Kraken{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Kraken")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &KrakenReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/sea-creatures/leviathan_controller.go
================================================
/*
Copyright 2026 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 seacreatures

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	seacreaturesv1beta2 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta2"
)

// LeviathanReconciler reconciles a Leviathan object
type LeviathanReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=leviathans,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=leviathans/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=leviathans/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Leviathan object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *LeviathanReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *LeviathanReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&seacreaturesv1beta2.Leviathan{}).
		Named("sea-creatures-leviathan").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/sea-creatures/leviathan_controller_test.go
================================================
/*
Copyright 2026 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 seacreatures

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	seacreaturesv1beta2 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta2"
)

var _ = Describe("Leviathan Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		leviathan := &seacreaturesv1beta2.Leviathan{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Leviathan")
			err := k8sClient.Get(ctx, typeNamespacedName, leviathan)
			if err != nil && errors.IsNotFound(err) {
				resource := &seacreaturesv1beta2.Leviathan{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &seacreaturesv1beta2.Leviathan{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Leviathan")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &LeviathanReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/sea-creatures/suite_test.go
================================================
/*
Copyright 2026 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 seacreatures

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	seacreaturesv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta1"
	seacreaturesv1beta2 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1beta2"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = seacreaturesv1beta1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	err = seacreaturesv1beta2.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/ship/cruiser_controller.go
================================================
/*
Copyright 2026 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 ship

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1"
)

// CruiserReconciler reconciles a Cruiser object
type CruiserReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=ship.testproject.org,resources=cruisers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ship.testproject.org,resources=cruisers/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ship.testproject.org,resources=cruisers/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Cruiser object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *CruiserReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *CruiserReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&shipv2alpha1.Cruiser{}).
		Named("ship-cruiser").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/ship/cruiser_controller_test.go
================================================
/*
Copyright 2026 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 ship

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1"
)

var _ = Describe("Cruiser Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		cruiser := &shipv2alpha1.Cruiser{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Cruiser")
			err := k8sClient.Get(ctx, typeNamespacedName, cruiser)
			if err != nil && errors.IsNotFound(err) {
				resource := &shipv2alpha1.Cruiser{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &shipv2alpha1.Cruiser{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Cruiser")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &CruiserReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/ship/destroyer_controller.go
================================================
/*
Copyright 2026 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 ship

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1"
)

// DestroyerReconciler reconciles a Destroyer object
type DestroyerReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=ship.testproject.org,resources=destroyers,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ship.testproject.org,resources=destroyers/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ship.testproject.org,resources=destroyers/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Destroyer object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *DestroyerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *DestroyerReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&shipv1.Destroyer{}).
		Named("ship-destroyer").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/ship/destroyer_controller_test.go
================================================
/*
Copyright 2026 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 ship

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1"
)

var _ = Describe("Destroyer Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		destroyer := &shipv1.Destroyer{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Destroyer")
			err := k8sClient.Get(ctx, typeNamespacedName, destroyer)
			if err != nil && errors.IsNotFound(err) {
				resource := &shipv1.Destroyer{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &shipv1.Destroyer{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Destroyer")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &DestroyerReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/ship/frigate_controller.go
================================================
/*
Copyright 2026 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 ship

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	shipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1"
)

// FrigateReconciler reconciles a Frigate object
type FrigateReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=ship.testproject.org,resources=frigates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=ship.testproject.org,resources=frigates/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=ship.testproject.org,resources=frigates/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Frigate object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *FrigateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *FrigateReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&shipv1beta1.Frigate{}).
		Named("ship-frigate").
		Complete(r)
}


================================================
FILE: testdata/project-v4-multigroup/internal/controller/ship/frigate_controller_test.go
================================================
/*
Copyright 2026 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 ship

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	shipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1"
)

var _ = Describe("Frigate Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		frigate := &shipv1beta1.Frigate{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Frigate")
			err := k8sClient.Get(ctx, typeNamespacedName, frigate)
			if err != nil && errors.IsNotFound(err) {
				resource := &shipv1beta1.Frigate{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &shipv1beta1.Frigate{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Frigate")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &FrigateReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-multigroup/internal/controller/ship/suite_test.go
================================================
/*
Copyright 2026 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 ship

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1"
	shipv1beta1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1beta1"
	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = shipv1beta1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	err = shipv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	err = shipv2alpha1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/apps/v1/deployment_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	appsv1 "k8s.io/api/apps/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// nolint:unused
// log is for logging in this package.
var deploymentlog = logf.Log.WithName("deployment-resource")

// SetupDeploymentWebhookWithManager registers the webhook for Deployment in the manager.
func SetupDeploymentWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &appsv1.Deployment{}).
		WithDefaulter(&DeploymentCustomDefaulter{}).
		WithValidator(&DeploymentCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-apps-v1-deployment,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=mdeployment-v1.kb.io,admissionReviewVersions=v1

// DeploymentCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Deployment when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type DeploymentCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Deployment.
func (d *DeploymentCustomDefaulter) Default(_ context.Context, obj *appsv1.Deployment) error {
	deploymentlog.Info("Defaulting for Deployment", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-apps-v1-deployment,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=vdeployment-v1.kb.io,admissionReviewVersions=v1

// DeploymentCustomValidator struct is responsible for validating the Deployment resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type DeploymentCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Deployment.
func (v *DeploymentCustomValidator) ValidateCreate(_ context.Context, obj *appsv1.Deployment) (admission.Warnings, error) {
	deploymentlog.Info("Validation for Deployment upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Deployment.
func (v *DeploymentCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *appsv1.Deployment) (admission.Warnings, error) {
	deploymentlog.Info("Validation for Deployment upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Deployment.
func (v *DeploymentCustomValidator) ValidateDelete(_ context.Context, obj *appsv1.Deployment) (admission.Warnings, error) {
	deploymentlog.Info("Validation for Deployment upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/apps/v1/deployment_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Deployment Webhook", func() {
	var (
		obj       *appsv1.Deployment
		oldObj    *appsv1.Deployment
		defaulter DeploymentCustomDefaulter
		validator DeploymentCustomValidator
	)

	BeforeEach(func() {
		obj = &appsv1.Deployment{}
		oldObj = &appsv1.Deployment{}
		defaulter = DeploymentCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
		validator = DeploymentCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Deployment under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

	Context("When creating or updating Deployment under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/apps/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = appsv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupDeploymentWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/cert-manager/v1/issuer_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
)

// nolint:unused
// log is for logging in this package.
var issuerlog = logf.Log.WithName("issuer-resource")

// SetupIssuerWebhookWithManager registers the webhook for Issuer in the manager.
func SetupIssuerWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &certmanagerv1.Issuer{}).
		WithDefaulter(&IssuerCustomDefaulter{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-cert-manager-io-v1-issuer,mutating=true,failurePolicy=fail,sideEffects=None,groups=cert-manager.io,resources=issuers,verbs=create;update,versions=v1,name=missuer-v1.kb.io,admissionReviewVersions=v1

// IssuerCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Issuer when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type IssuerCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Issuer.
func (d *IssuerCustomDefaulter) Default(_ context.Context, obj *certmanagerv1.Issuer) error {
	issuerlog.Info("Defaulting for Issuer", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/cert-manager/v1/issuer_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Issuer Webhook", func() {
	var (
		obj       *certmanagerv1.Issuer
		oldObj    *certmanagerv1.Issuer
		defaulter IssuerCustomDefaulter
	)

	BeforeEach(func() {
		obj = &certmanagerv1.Issuer{}
		oldObj = &certmanagerv1.Issuer{}
		defaulter = IssuerCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Issuer under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/cert-manager/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = certmanagerv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupIssuerWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	corev1 "k8s.io/api/core/v1"
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// nolint:unused
// log is for logging in this package.
var podlog = logf.Log.WithName("pod-resource")

// SetupPodWebhookWithManager registers the webhook for Pod in the manager.
func SetupPodWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &corev1.Pod{}).
		WithValidator(&PodCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate--v1-pod,mutating=false,failurePolicy=fail,sideEffects=None,groups="",resources=pods,verbs=create;update,versions=v1,name=vpod-v1.kb.io,admissionReviewVersions=v1

// PodCustomValidator struct is responsible for validating the Pod resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type PodCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Pod.
func (v *PodCustomValidator) ValidateCreate(_ context.Context, obj *corev1.Pod) (admission.Warnings, error) {
	podlog.Info("Validation for Pod upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Pod.
func (v *PodCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *corev1.Pod) (admission.Warnings, error) {
	podlog.Info("Validation for Pod upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Pod.
func (v *PodCustomValidator) ValidateDelete(_ context.Context, obj *corev1.Pod) (admission.Warnings, error) {
	podlog.Info("Validation for Pod upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/core/v1/pod_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	corev1 "k8s.io/api/core/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Pod Webhook", func() {
	var (
		obj       *corev1.Pod
		oldObj    *corev1.Pod
		validator PodCustomValidator
	)

	BeforeEach(func() {
		obj = &corev1.Pod{}
		oldObj = &corev1.Pod{}
		validator = PodCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating or updating Pod under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/core/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	corev1 "k8s.io/api/core/v1"
	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = corev1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupPodWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1"

	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// nolint:unused
// log is for logging in this package.
var captainlog = logf.Log.WithName("captain-resource")

// SetupCaptainWebhookWithManager registers the webhook for Captain in the manager.
func SetupCaptainWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &crewv1.Captain{}).
		WithDefaulter(&CaptainCustomDefaulter{}).
		WithValidator(&CaptainCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-crew-testproject-org-v1-captain,mutating=true,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=mcaptain-v1.kb.io,admissionReviewVersions=v1

// CaptainCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Captain when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type CaptainCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Captain.
func (d *CaptainCustomDefaulter) Default(_ context.Context, obj *crewv1.Captain) error {
	captainlog.Info("Defaulting for Captain", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-crew-testproject-org-v1-captain,mutating=false,failurePolicy=fail,sideEffects=None,groups=crew.testproject.org,resources=captains,verbs=create;update,versions=v1,name=vcaptain-v1.kb.io,admissionReviewVersions=v1

// CaptainCustomValidator struct is responsible for validating the Captain resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type CaptainCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Captain.
func (v *CaptainCustomValidator) ValidateCreate(_ context.Context, obj *crewv1.Captain) (admission.Warnings, error) {
	captainlog.Info("Validation for Captain upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Captain.
func (v *CaptainCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *crewv1.Captain) (admission.Warnings, error) {
	captainlog.Info("Validation for Captain upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Captain.
func (v *CaptainCustomValidator) ValidateDelete(_ context.Context, obj *crewv1.Captain) (admission.Warnings, error) {
	captainlog.Info("Validation for Captain upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/crew/v1/captain_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Captain Webhook", func() {
	var (
		obj       *crewv1.Captain
		oldObj    *crewv1.Captain
		defaulter CaptainCustomDefaulter
		validator CaptainCustomValidator
	)

	BeforeEach(func() {
		obj = &crewv1.Captain{}
		oldObj = &crewv1.Captain{}
		defaulter = CaptainCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
		validator = CaptainCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Captain under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

	Context("When creating or updating Captain under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/crew/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/crew/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = crewv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupCaptainWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/example.com/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = examplecomv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupWordpressWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/example.com/v1/wordpress_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
)

// nolint:unused
// log is for logging in this package.
var wordpresslog = logf.Log.WithName("wordpress-resource")

// SetupWordpressWebhookWithManager registers the webhook for Wordpress in the manager.
func SetupWordpressWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &examplecomv1.Wordpress{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/example.com/v1/wordpress_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Wordpress Webhook", func() {
	var (
		obj    *examplecomv1.Wordpress
		oldObj *examplecomv1.Wordpress
	)

	BeforeEach(func() {
		obj = &examplecomv1.Wordpress{}
		oldObj = &examplecomv1.Wordpress{}
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Wordpress under Conversion Webhook", func() {
		// TODO (user): Add logic to convert the object to the desired version and verify the conversion
		// Example:
		// It("Should convert the object correctly", func() {
		//     convertedObj := &examplecomv1.Wordpress{}
		//     Expect(obj.ConvertTo(convertedObj)).To(Succeed())
		//     Expect(convertedObj).ToNot(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
)

// nolint:unused
// log is for logging in this package.
var memcachedlog = logf.Log.WithName("memcached-resource")

// SetupMemcachedWebhookWithManager registers the webhook for Memcached in the manager.
func SetupMemcachedWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &examplecomv1alpha1.Memcached{}).
		WithValidator(&MemcachedCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached-v1alpha1.kb.io,admissionReviewVersions=v1

// MemcachedCustomValidator struct is responsible for validating the Memcached resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type MemcachedCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Memcached.
func (v *MemcachedCustomValidator) ValidateCreate(_ context.Context, obj *examplecomv1alpha1.Memcached) (admission.Warnings, error) {
	memcachedlog.Info("Validation for Memcached upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Memcached.
func (v *MemcachedCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *examplecomv1alpha1.Memcached) (admission.Warnings, error) {
	memcachedlog.Info("Validation for Memcached upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Memcached.
func (v *MemcachedCustomValidator) ValidateDelete(_ context.Context, obj *examplecomv1alpha1.Memcached) (admission.Warnings, error) {
	memcachedlog.Info("Validation for Memcached upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/memcached_webhook_test.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Memcached Webhook", func() {
	var (
		obj       *examplecomv1alpha1.Memcached
		oldObj    *examplecomv1alpha1.Memcached
		validator MemcachedCustomValidator
	)

	BeforeEach(func() {
		obj = &examplecomv1alpha1.Memcached{}
		oldObj = &examplecomv1alpha1.Memcached{}
		validator = MemcachedCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating or updating Memcached under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/example.com/v1alpha1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/example.com/v1alpha1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = examplecomv1alpha1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupMemcachedWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1"
)

// nolint:unused
// log is for logging in this package.
var destroyerlog = logf.Log.WithName("destroyer-resource")

// SetupDestroyerWebhookWithManager registers the webhook for Destroyer in the manager.
func SetupDestroyerWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &shipv1.Destroyer{}).
		WithDefaulter(&DestroyerCustomDefaulter{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// +kubebuilder:webhook:path=/mutate-ship-testproject-org-v1-destroyer,mutating=true,failurePolicy=fail,sideEffects=None,groups=ship.testproject.org,resources=destroyers,verbs=create;update,versions=v1,name=mdestroyer-v1.kb.io,admissionReviewVersions=v1

// DestroyerCustomDefaulter struct is responsible for setting default values on the custom resource of the
// Kind Destroyer when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type DestroyerCustomDefaulter struct {
	// TODO(user): Add more fields as needed for defaulting
}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind Destroyer.
func (d *DestroyerCustomDefaulter) Default(_ context.Context, obj *shipv1.Destroyer) error {
	destroyerlog.Info("Defaulting for Destroyer", "name", obj.GetName())

	// TODO(user): fill in your defaulting logic.

	return nil
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/ship/v1/destroyer_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Destroyer Webhook", func() {
	var (
		obj       *shipv1.Destroyer
		oldObj    *shipv1.Destroyer
		defaulter DestroyerCustomDefaulter
	)

	BeforeEach(func() {
		obj = &shipv1.Destroyer{}
		oldObj = &shipv1.Destroyer{}
		defaulter = DestroyerCustomDefaulter{}
		Expect(defaulter).NotTo(BeNil(), "Expected defaulter to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Destroyer under Defaulting Webhook", func() {
		// TODO (user): Add logic for defaulting webhooks
		// Example:
		// It("Should apply defaults when a required field is empty", func() {
		//     By("simulating a scenario where defaults should be applied")
		//     obj.SomeFieldWithDefault = ""
		//     By("calling the Default method to apply defaults")
		//     defaulter.Default(ctx, obj)
		//     By("checking that the default values are set")
		//     Expect(obj.SomeFieldWithDefault).To(Equal("default_value"))
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/ship/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	shipv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = shipv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupDestroyerWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook.go
================================================
/*
Copyright 2026 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 v2alpha1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1"
)

// nolint:unused
// log is for logging in this package.
var cruiserlog = logf.Log.WithName("cruiser-resource")

// SetupCruiserWebhookWithManager registers the webhook for Cruiser in the manager.
func SetupCruiserWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &shipv2alpha1.Cruiser{}).
		WithValidator(&CruiserCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-ship-testproject-org-v2alpha1-cruiser,mutating=false,failurePolicy=fail,sideEffects=None,groups=ship.testproject.org,resources=cruisers,verbs=create;update,versions=v2alpha1,name=vcruiser-v2alpha1.kb.io,admissionReviewVersions=v1

// CruiserCustomValidator struct is responsible for validating the Cruiser resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type CruiserCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Cruiser.
func (v *CruiserCustomValidator) ValidateCreate(_ context.Context, obj *shipv2alpha1.Cruiser) (admission.Warnings, error) {
	cruiserlog.Info("Validation for Cruiser upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Cruiser.
func (v *CruiserCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *shipv2alpha1.Cruiser) (admission.Warnings, error) {
	cruiserlog.Info("Validation for Cruiser upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Cruiser.
func (v *CruiserCustomValidator) ValidateDelete(_ context.Context, obj *shipv2alpha1.Cruiser) (admission.Warnings, error) {
	cruiserlog.Info("Validation for Cruiser upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/cruiser_webhook_test.go
================================================
/*
Copyright 2026 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 v2alpha1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Cruiser Webhook", func() {
	var (
		obj       *shipv2alpha1.Cruiser
		oldObj    *shipv2alpha1.Cruiser
		validator CruiserCustomValidator
	)

	BeforeEach(func() {
		obj = &shipv2alpha1.Cruiser{}
		oldObj = &shipv2alpha1.Cruiser{}
		validator = CruiserCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating or updating Cruiser under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-multigroup/internal/webhook/ship/v2alpha1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v2alpha1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	shipv2alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/ship/v2alpha1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = shipv2alpha1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupCruiserWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-multigroup/test/e2e/e2e_suite_test.go
================================================
//go:build e2e
// +build e2e

/*
Copyright 2026 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 e2e

import (
	"fmt"
	"os"
	"os/exec"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/test/utils"
)

var (
	// managerImage is the manager image to be built and loaded for testing.
	managerImage = "example.com/project-v4-multigroup:v0.0.1"
	// shouldCleanupCertManager tracks whether CertManager was installed by this suite.
	shouldCleanupCertManager = false
)

// TestE2E runs the e2e test suite to validate the solution in an isolated environment.
// The default setup requires Kind and CertManager.
//
// To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true
func TestE2E(t *testing.T) {
	RegisterFailHandler(Fail)
	_, _ = fmt.Fprintf(GinkgoWriter, "Starting project-v4-multigroup e2e test suite\n")
	RunSpecs(t, "e2e suite")
}

var _ = BeforeSuite(func() {
	By("building the manager image")
	cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage))
	_, err := utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image")

	// TODO(user): If you want to change the e2e test vendor from Kind,
	// ensure the image is built and available, then remove the following block.
	By("loading the manager image on Kind")
	err = utils.LoadImageToKindClusterWithName(managerImage)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind")

	setupCertManager()
})

var _ = AfterSuite(func() {
	teardownCertManager()
})

// setupCertManager installs CertManager if needed for webhook tests.
// Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present.
func setupCertManager() {
	if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n")
		return
	}

	By("checking if CertManager is already installed")
	if utils.IsCertManagerCRDsInstalled() {
		_, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n")
		return
	}

	// Mark for cleanup before installation to handle interruptions and partial installs.
	shouldCleanupCertManager = true

	By("installing CertManager")
	Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
}

// teardownCertManager uninstalls CertManager if it was installed by setupCertManager.
// This ensures we only remove what we installed.
func teardownCertManager() {
	if !shouldCleanupCertManager {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n")
		return
	}

	By("uninstalling CertManager")
	utils.UninstallCertManager()
}


================================================
FILE: testdata/project-v4-multigroup/test/e2e/e2e_test.go
================================================
//go:build e2e
// +build e2e

/*
Copyright 2026 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 e2e

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/test/utils"
)

// namespace where the project is deployed in
const namespace = "project-v4-multigroup-system"

// serviceAccountName created for the project
const serviceAccountName = "project-v4-multigroup-controller-manager"

// metricsServiceName is the name of the metrics service of the project
const metricsServiceName = "project-v4-multigroup-controller-manager-metrics-service"

// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
const metricsRoleBindingName = "project-v4-multigroup-metrics-binding"

var _ = Describe("Manager", Ordered, func() {
	var controllerPodName string

	// Before running the tests, set up the environment by creating the namespace,
	// enforce the restricted security policy to the namespace, installing CRDs,
	// and deploying the controller.
	BeforeAll(func() {
		By("creating manager namespace")
		cmd := exec.Command("kubectl", "create", "ns", namespace)
		_, err := utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")

		By("labeling the namespace to enforce the restricted security policy")
		cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
			"pod-security.kubernetes.io/enforce=restricted")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")

		By("installing CRDs")
		cmd = exec.Command("make", "install")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")

		By("deploying the controller-manager")
		cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage))
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
	})

	// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
	// and deleting the namespace.
	AfterAll(func() {
		By("cleaning up the curl pod for metrics")
		cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
		_, _ = utils.Run(cmd)

		By("undeploying the controller-manager")
		cmd = exec.Command("make", "undeploy")
		_, _ = utils.Run(cmd)

		By("uninstalling CRDs")
		cmd = exec.Command("make", "uninstall")
		_, _ = utils.Run(cmd)

		By("removing manager namespace")
		cmd = exec.Command("kubectl", "delete", "ns", namespace)
		_, _ = utils.Run(cmd)
	})

	// After each test, check for failures and collect logs, events,
	// and pod descriptions for debugging.
	AfterEach(func() {
		specReport := CurrentSpecReport()
		if specReport.Failed() {
			By("Fetching controller manager pod logs")
			cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
			controllerLogs, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
			}

			By("Fetching Kubernetes events")
			cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
			eventsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
			}

			By("Fetching curl-metrics logs")
			cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
			metricsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
			}

			By("Fetching controller manager pod description")
			cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
			podDescription, err := utils.Run(cmd)
			if err == nil {
				fmt.Println("Pod description:\n", podDescription)
			} else {
				fmt.Println("Failed to describe controller pod")
			}
		}
	})

	SetDefaultEventuallyTimeout(2 * time.Minute)
	SetDefaultEventuallyPollingInterval(time.Second)

	Context("Manager", func() {
		It("should run successfully", func() {
			By("validating that the controller-manager pod is running as expected")
			verifyControllerUp := func(g Gomega) {
				// Get the name of the controller-manager pod
				cmd := exec.Command("kubectl", "get",
					"pods", "-l", "control-plane=controller-manager",
					"-o", "go-template={{ range .items }}"+
						"{{ if not .metadata.deletionTimestamp }}"+
						"{{ .metadata.name }}"+
						"{{ \"\\n\" }}{{ end }}{{ end }}",
					"-n", namespace,
				)

				podOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
				podNames := utils.GetNonEmptyLines(podOutput)
				g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
				controllerPodName = podNames[0]
				g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))

				// Validate the pod's status
				cmd = exec.Command("kubectl", "get",
					"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
					"-n", namespace,
				)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
			}
			Eventually(verifyControllerUp).Should(Succeed())
		})

		It("should ensure the metrics endpoint is serving metrics", func() {
			By("creating a ClusterRoleBinding for the service account to allow access to metrics")
			cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
				"--clusterrole=project-v4-multigroup-metrics-reader",
				fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
			)
			_, err := utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")

			By("validating that the metrics service is available")
			cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")

			By("getting the service account token")
			token, err := serviceAccountToken()
			Expect(err).NotTo(HaveOccurred())
			Expect(token).NotTo(BeEmpty())

			By("ensuring the controller pod is ready")
			verifyControllerPodReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace,
					"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("True"), "Controller pod not ready")
			}
			Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying that the controller manager is serving the metrics server")
			verifyMetricsServerStarted := func(g Gomega) {
				cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(ContainSubstring("Serving metrics server"),
					"Metrics server not yet started")
			}
			Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed())

			By("waiting for the webhook service endpoints to be ready")
			verifyWebhookEndpointsReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "endpointslices.discovery.k8s.io", "-n", namespace,
					"-l", "kubernetes.io/service-name=project-v4-multigroup-webhook-service",
					"-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist")
				g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready")
			}
			Eventually(verifyWebhookEndpointsReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying the mutating webhook server is ready")
			verifyMutatingWebhookReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "mutatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-multigroup-mutating-webhook-configuration",
					"-o", "jsonpath={.webhooks[0].clientConfig.caBundle}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "MutatingWebhookConfiguration should exist")
				g.Expect(output).ShouldNot(BeEmpty(), "Mutating webhook CA bundle not yet injected")
			}
			Eventually(verifyMutatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed())

			By("waiting additional time for webhook server to stabilize")
			time.Sleep(5 * time.Second)

			// +kubebuilder:scaffold:e2e-metrics-webhooks-readiness

			By("creating the curl-metrics pod to access the metrics endpoint")
			cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
				"--namespace", namespace,
				"--image=curlimages/curl:latest",
				"--overrides",
				fmt.Sprintf(`{
					"spec": {
						"containers": [{
							"name": "curl",
							"image": "curlimages/curl:latest",
							"command": ["/bin/sh", "-c"],
							"args": [
								"for i in $(seq 1 30); do curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics && exit 0 || sleep 2; done; exit 1"
							],
							"securityContext": {
								"readOnlyRootFilesystem": true,
								"allowPrivilegeEscalation": false,
								"capabilities": {
									"drop": ["ALL"]
								},
								"runAsNonRoot": true,
								"runAsUser": 1000,
								"seccompProfile": {
									"type": "RuntimeDefault"
								}
							}
						}],
						"serviceAccountName": "%s"
					}
				}`, token, metricsServiceName, namespace, serviceAccountName))
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")

			By("waiting for the curl-metrics pod to complete.")
			verifyCurlUp := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
					"-o", "jsonpath={.status.phase}",
					"-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
			}
			Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())

			By("getting the metrics by checking curl-metrics logs")
			verifyMetricsAvailable := func(g Gomega) {
				metricsOutput, err := getMetricsOutput()
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
				g.Expect(metricsOutput).NotTo(BeEmpty())
				g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
			}
			Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
		})

		It("should provisioned cert-manager", func() {
			By("validating that cert-manager has the certificate Secret")
			verifyCertManager := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace)
				_, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
			}
			Eventually(verifyCertManager).Should(Succeed())
		})

		It("should have CA injection for mutating webhooks", func() {
			By("checking CA injection for mutating webhooks")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"mutatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-multigroup-mutating-webhook-configuration",
					"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
				mwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(mwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		It("should have CA injection for validating webhooks", func() {
			By("checking CA injection for validating webhooks")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"validatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-multigroup-validating-webhook-configuration",
					"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
				vwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		It("should have CA injection for Wordpress conversion webhook", func() {
			By("checking CA injection for Wordpress conversion webhook")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"customresourcedefinitions.apiextensions.k8s.io",
					"wordpresses.example.com.testproject.org",
					"-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}")
				vwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		// +kubebuilder:scaffold:e2e-webhooks-checks

		// TODO: Customize the e2e test suite with scenarios specific to your project.
		// Consider applying sample/CR(s) and check their status and/or verifying
		// the reconciliation by using the metrics, i.e.:
		// metricsOutput, err := getMetricsOutput()
		// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
		// Expect(metricsOutput).To(ContainSubstring(
		//    fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
		//    strings.ToLower(),
		// ))
	})
})

// serviceAccountToken returns a token for the specified service account in the given namespace.
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
// and parsing the resulting token from the API response.
func serviceAccountToken() (string, error) {
	const tokenRequestRawString = `{
		"apiVersion": "authentication.k8s.io/v1",
		"kind": "TokenRequest"
	}`

	// Temporary file to store the token request
	secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
	tokenRequestFile := filepath.Join("/tmp", secretName)
	err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
	if err != nil {
		return "", err
	}

	var out string
	verifyTokenCreation := func(g Gomega) {
		// Execute kubectl command to create the token
		cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
			"/api/v1/namespaces/%s/serviceaccounts/%s/token",
			namespace,
			serviceAccountName,
		), "-f", tokenRequestFile)

		output, err := cmd.CombinedOutput()
		g.Expect(err).NotTo(HaveOccurred())

		// Parse the JSON output to extract the token
		var token tokenRequest
		err = json.Unmarshal(output, &token)
		g.Expect(err).NotTo(HaveOccurred())

		out = token.Status.Token
	}
	Eventually(verifyTokenCreation).Should(Succeed())

	return out, err
}

// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
func getMetricsOutput() (string, error) {
	By("getting the curl-metrics logs")
	cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
	return utils.Run(cmd)
}

// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
// containing only the token field that we need to extract.
type tokenRequest struct {
	Status struct {
		Token string `json:"token"`
	} `json:"status"`
}


================================================
FILE: testdata/project-v4-multigroup/test/utils/utils.go
================================================
/*
Copyright 2026 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 utils

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"

	. "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
)

const (
	certmanagerVersion = "v1.20.0"
	certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"

	defaultKindBinary  = "kind"
	defaultKindCluster = "kind"
)

func warnError(err error) {
	_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
}

// Run executes the provided command within this context
func Run(cmd *exec.Cmd) (string, error) {
	dir, _ := GetProjectDir()
	cmd.Dir = dir

	if err := os.Chdir(cmd.Dir); err != nil {
		_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
	}

	cmd.Env = append(os.Environ(), "GO111MODULE=on")
	command := strings.Join(cmd.Args, " ")
	_, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
	}

	return string(output), nil
}

// UninstallCertManager uninstalls the cert manager
func UninstallCertManager() {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "delete", "-f", url)
	if _, err := Run(cmd); err != nil {
		warnError(err)
	}

	// Delete leftover leases in kube-system (not cleaned by default)
	kubeSystemLeases := []string{
		"cert-manager-cainjector-leader-election",
		"cert-manager-controller",
	}
	for _, lease := range kubeSystemLeases {
		cmd = exec.Command("kubectl", "delete", "lease", lease,
			"-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
		if _, err := Run(cmd); err != nil {
			warnError(err)
		}
	}
}

// InstallCertManager installs the cert manager bundle.
func InstallCertManager() error {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "apply", "-f", url)
	if _, err := Run(cmd); err != nil {
		return err
	}
	// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
	// was re-installed after uninstalling on a cluster.
	cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
		"--for", "condition=Available",
		"--namespace", "cert-manager",
		"--timeout", "5m",
	)

	_, err := Run(cmd)
	return err
}

// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
// by verifying the existence of key CRDs related to Cert Manager.
func IsCertManagerCRDsInstalled() bool {
	// List of common Cert Manager CRDs
	certManagerCRDs := []string{
		"certificates.cert-manager.io",
		"issuers.cert-manager.io",
		"clusterissuers.cert-manager.io",
		"certificaterequests.cert-manager.io",
		"orders.acme.cert-manager.io",
		"challenges.acme.cert-manager.io",
	}

	// Execute the kubectl command to get all CRDs
	cmd := exec.Command("kubectl", "get", "crds")
	output, err := Run(cmd)
	if err != nil {
		return false
	}

	// Check if any of the Cert Manager CRDs are present
	crdList := GetNonEmptyLines(output)
	for _, crd := range certManagerCRDs {
		for _, line := range crdList {
			if strings.Contains(line, crd) {
				return true
			}
		}
	}

	return false
}

// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
func LoadImageToKindClusterWithName(name string) error {
	cluster := defaultKindCluster
	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
		cluster = v
	}
	kindOptions := []string{"load", "docker-image", name, "--name", cluster}
	kindBinary := defaultKindBinary
	if v, ok := os.LookupEnv("KIND"); ok {
		kindBinary = v
	}
	cmd := exec.Command(kindBinary, kindOptions...)
	_, err := Run(cmd)
	return err
}

// GetNonEmptyLines converts given command output string into individual objects
// according to line breakers, and ignores the empty elements in it.
func GetNonEmptyLines(output string) []string {
	var res []string
	elements := strings.SplitSeq(output, "\n")
	for element := range elements {
		if element != "" {
			res = append(res, element)
		}
	}

	return res
}

// GetProjectDir will return the directory where the project is
func GetProjectDir() (string, error) {
	wd, err := os.Getwd()
	if err != nil {
		return wd, fmt.Errorf("failed to get current working directory: %w", err)
	}
	wd = strings.ReplaceAll(wd, "/test/e2e", "")
	return wd, nil
}

// UncommentCode searches for target in the file and remove the comment prefix
// of the target content. The target content may span multiple lines.
func UncommentCode(filename, target, prefix string) error {
	// false positive
	// nolint:gosec
	content, err := os.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("failed to read file %q: %w", filename, err)
	}
	strContent := string(content)

	idx := strings.Index(strContent, target)
	if idx < 0 {
		return fmt.Errorf("unable to find the code %q to be uncommented", target)
	}

	out := new(bytes.Buffer)
	_, err = out.Write(content[:idx])
	if err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	scanner := bufio.NewScanner(bytes.NewBufferString(target))
	if !scanner.Scan() {
		return nil
	}
	for {
		if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
		// Avoid writing a newline in case the previous line was the last in target.
		if !scanner.Scan() {
			break
		}
		if _, err = out.WriteString("\n"); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
	}

	if _, err = out.Write(content[idx+len(target):]); err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	// false positive
	// nolint:gosec
	if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
		return fmt.Errorf("failed to write file %q: %w", filename, err)
	}

	return nil
}


================================================
FILE: testdata/project-v4-with-plugins/.custom-gcl.yml
================================================
# This file configures golangci-lint with module plugins.
# When you run 'make lint', it will automatically build a custom golangci-lint binary
# with all the plugins listed below.
#
# See: https://golangci-lint.run/plugins/module-plugins/
version: v2.8.0
plugins:
  # logcheck validates structured logging calls and parameters (e.g., balanced key-value pairs)
  - module: "sigs.k8s.io/logtools"
    import: "sigs.k8s.io/logtools/logcheck/gclplugin"
    version: latest


================================================
FILE: testdata/project-v4-with-plugins/.devcontainer/devcontainer.json
================================================
{
  "name": "Kubebuilder DevContainer",
  "image": "golang:1.25",
  "features": {
    "ghcr.io/devcontainers/features/docker-in-docker:2": {
      "moby": false,
      "dockerDefaultAddressPool": "base=172.30.0.0/16,size=24"
    },
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/common-utils:2": {
      "upgradePackages": true
    }
  },

  "runArgs": ["--privileged", "--init"],

  "customizations": {
    "vscode": {
      "settings": {
        "terminal.integrated.shell.linux": "/bin/bash"
      },
      "extensions": [
        "ms-kubernetes-tools.vscode-kubernetes-tools",
        "ms-azuretools.vscode-docker"
      ]
    }
  },

  "remoteEnv": {
    "GO111MODULE": "on"
  },

  "onCreateCommand": "bash .devcontainer/post-install.sh"
}



================================================
FILE: testdata/project-v4-with-plugins/.devcontainer/post-install.sh
================================================
#!/bin/bash
set -euo pipefail

echo "===================================="
echo "Kubebuilder DevContainer Setup"
echo "===================================="

# Verify running as root (required for installing to /usr/local/bin and /etc)
if [ "$(id -u)" -ne 0 ]; then
  echo "ERROR: This script must be run as root"
  exit 1
fi

echo ""
echo "Detecting system architecture..."
# Detect architecture using uname
MACHINE=$(uname -m)
case "${MACHINE}" in
  x86_64)
    ARCH="amd64"
    ;;
  aarch64|arm64)
    ARCH="arm64"
    ;;
  *)
    echo "WARNING: Unsupported architecture ${MACHINE}, defaulting to amd64"
    ARCH="amd64"
    ;;
esac
echo "Architecture: ${ARCH}"

echo ""
echo "------------------------------------"
echo "Setting up bash completion..."
echo "------------------------------------"

BASH_COMPLETIONS_DIR="/usr/share/bash-completion/completions"

# Enable bash-completion in root's .bashrc (devcontainer runs as root)
if ! grep -q "source /usr/share/bash-completion/bash_completion" ~/.bashrc 2>/dev/null; then
  echo 'source /usr/share/bash-completion/bash_completion' >> ~/.bashrc
  echo "Added bash-completion to .bashrc"
fi

echo ""
echo "------------------------------------"
echo "Installing development tools..."
echo "------------------------------------"

# Install kind
if ! command -v kind &> /dev/null; then
  echo "Installing kind..."
  curl -Lo /usr/local/bin/kind "https://kind.sigs.k8s.io/dl/latest/kind-linux-${ARCH}"
  chmod +x /usr/local/bin/kind
  echo "kind installed successfully"
fi

# Generate kind bash completion
if command -v kind &> /dev/null; then
  if kind completion bash > "${BASH_COMPLETIONS_DIR}/kind" 2>/dev/null; then
    echo "kind completion installed"
  else
    echo "WARNING: Failed to generate kind completion"
  fi
fi

# Install kubebuilder
if ! command -v kubebuilder &> /dev/null; then
  echo "Installing kubebuilder..."
  curl -Lo /usr/local/bin/kubebuilder "https://go.kubebuilder.io/dl/latest/linux/${ARCH}"
  chmod +x /usr/local/bin/kubebuilder
  echo "kubebuilder installed successfully"
fi

# Generate kubebuilder bash completion
if command -v kubebuilder &> /dev/null; then
  if kubebuilder completion bash > "${BASH_COMPLETIONS_DIR}/kubebuilder" 2>/dev/null; then
    echo "kubebuilder completion installed"
  else
    echo "WARNING: Failed to generate kubebuilder completion"
  fi
fi

# Install kubectl
if ! command -v kubectl &> /dev/null; then
  echo "Installing kubectl..."
  KUBECTL_VERSION=$(curl -Ls https://dl.k8s.io/release/stable.txt)
  curl -Lo /usr/local/bin/kubectl "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${ARCH}/kubectl"
  chmod +x /usr/local/bin/kubectl
  echo "kubectl installed successfully"
fi

# Generate kubectl bash completion
if command -v kubectl &> /dev/null; then
  if kubectl completion bash > "${BASH_COMPLETIONS_DIR}/kubectl" 2>/dev/null; then
    echo "kubectl completion installed"
  else
    echo "WARNING: Failed to generate kubectl completion"
  fi
fi

# Generate Docker bash completion
if command -v docker &> /dev/null; then
  if docker completion bash > "${BASH_COMPLETIONS_DIR}/docker" 2>/dev/null; then
    echo "docker completion installed"
  else
    echo "WARNING: Failed to generate docker completion"
  fi
fi

echo ""
echo "------------------------------------"
echo "Configuring Docker environment..."
echo "------------------------------------"

# Wait for Docker to be ready
echo "Waiting for Docker to be ready..."
for i in {1..30}; do
  if docker info >/dev/null 2>&1; then
    echo "Docker is ready"
    break
  fi
  if [ "$i" -eq 30 ]; then
    echo "WARNING: Docker not ready after 30s"
  fi
  sleep 1
done

# Create kind network (ignore if already exists)
if ! docker network inspect kind >/dev/null 2>&1; then
  if docker network create kind >/dev/null 2>&1; then
    echo "Created kind network"
  else
    echo "WARNING: Failed to create kind network (may already exist)"
  fi
fi

echo ""
echo "------------------------------------"
echo "Verifying installations..."
echo "------------------------------------"
kind version
kubebuilder version
kubectl version --client
docker --version
go version

echo ""
echo "===================================="
echo "DevContainer ready!"
echo "===================================="
echo "All development tools installed successfully."
echo "You can now start building Kubernetes operators."


================================================
FILE: testdata/project-v4-with-plugins/.dockerignore
================================================
# More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file
# Ignore everything by default and re-include only needed files
**

# Re-include Go source files (but not *_test.go)
!**/*.go
**/*_test.go

# Re-include Go module files
!go.mod
!go.sum


================================================
FILE: testdata/project-v4-with-plugins/.github/workflows/auto_update.yml
================================================
name: Auto Update

# The 'kubebuilder alpha update' command requires write access to the repository to create a branch
# with the update files and allow you to open a pull request using the link provided in the issue.
# The branch created will be named in the format kubebuilder-update-from--to- by default.
# To protect your codebase, please ensure that you have branch protection rules configured for your 
# main branches. This will guarantee that no one can bypass a review and push directly to a branch like 'main'.
permissions:
  contents: write  # Create and push the update branch
  issues: write    # Create GitHub Issue with PR link
  models: read     # Use GitHub Models for AI summaries

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * 2" # Every Tuesday at 00:00 UTC

jobs:
  auto-update:
    runs-on: ubuntu-latest
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    # Checkout the repository.
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4
      with:
        token: ${{ secrets.GITHUB_TOKEN }}
        fetch-depth: 0

    # Configure Git to create commits with the GitHub Actions bot.
    - name: Configure Git
      run: |
        git config --global user.name "github-actions[bot]"
        git config --global user.email "github-actions[bot]@users.noreply.github.com"

    # Set up Go environment.
    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: stable

    # Install Kubebuilder.
    - name: Install Kubebuilder
      run: |
        curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
        chmod +x kubebuilder
        sudo mv kubebuilder /usr/local/bin/
        kubebuilder version

    # Install Models extension for GitHub CLI.
    - name: Install gh-models extension
      run: |
        gh extension install github/gh-models --force
        gh models --help >/dev/null

    # Run the Kubebuilder alpha update command.
    # More info: https://kubebuilder.io/reference/commands/alpha_update
    - name: Run kubebuilder alpha update
      # Executes the update command with specified flags.
      # --force: Completes the merge even if conflicts occur, leaving conflict markers.
      # --push: Automatically pushes the resulting output branch to the 'origin' remote.
      # --restore-path: Preserves specified paths (e.g., CI workflow files) when squashing.
      # --open-gh-issue: Creates a GitHub Issue with a link for opening a PR for review.
      # --use-gh-models: Adds an AI-generated comment to the created Issue with
      #   a short overview of the scaffold changes and conflict-resolution guidance (if any).
      run: |
        kubebuilder alpha update \
          --force \
          --push \
          --restore-path .github/workflows \
          --open-gh-issue \
          --use-gh-models


================================================
FILE: testdata/project-v4-with-plugins/.github/workflows/lint.yml
================================================
name: Lint

on:
  push:
  pull_request:

jobs:
  lint:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Check linter configuration
        run: make lint-config
      - name: Run linter
        run: make lint


================================================
FILE: testdata/project-v4-with-plugins/.github/workflows/test-chart.yml
================================================
name: Test Chart

on:
  push:
  pull_request:

jobs:
  test-e2e:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install the latest version of kind
        run: |
          curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind

      - name: Verify kind installation
        run: kind version

      - name: Create kind cluster
        run: kind create cluster

      - name: Prepare project-v4-with-plugins
        run: |
          go mod tidy
          make docker-build IMG=controller:latest
          kind load docker-image controller:latest

      - name: Install Helm
        run: make install-helm

      - name: Lint Helm Chart
        run: |
          helm lint ./dist/chart


      - name: Install cert-manager via Helm (wait for readiness)
        run: |
          helm repo add jetstack https://charts.jetstack.io
          helm repo update
          helm install cert-manager jetstack/cert-manager \
            --namespace cert-manager \
            --create-namespace \
            --set crds.enabled=true \
            --wait \
            --timeout 300s

# TODO: Uncomment if Prometheus is enabled
#      - name: Install Prometheus Operator CRDs
#        run: |
#          helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
#          helm repo update
#          helm install prometheus-crds prometheus-community/prometheus-operator-crds

      - name: Deploy manager via Helm
        run: |
          make helm-deploy IMG=project-v4-with-plugins:v0.1.0

      - name: Check Helm release status
        run: |
          make helm-status


================================================
FILE: testdata/project-v4-with-plugins/.github/workflows/test-e2e.yml
================================================
name: E2E Tests

on:
  push:
  pull_request:

jobs:
  test-e2e:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Install the latest version of kind
        run: |
          curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-$(go env GOARCH)
          chmod +x ./kind
          sudo mv ./kind /usr/local/bin/kind

      - name: Verify kind installation
        run: kind version

      - name: Running Test e2e
        run: |
          go mod tidy
          make test-e2e


================================================
FILE: testdata/project-v4-with-plugins/.github/workflows/test.yml
================================================
name: Tests

on:
  push:
  pull_request:

jobs:
  test:
    name: Run on Ubuntu
    runs-on: ubuntu-latest
    steps:
      - name: Clone the code
        uses: actions/checkout@v4

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Running Tests
        run: |
          go mod tidy
          make test


================================================
FILE: testdata/project-v4-with-plugins/.gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
bin/*
Dockerfile.cross

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Go workspace file
go.work

# Kubernetes Generated files - skip generated files, except for vendored files
!vendor/**/zz_generated.*

# editor and IDE paraphernalia
.idea
.vscode
*.swp
*.swo
*~

# Kubeconfig might contain secrets
*.kubeconfig


================================================
FILE: testdata/project-v4-with-plugins/.golangci.yml
================================================
version: "2"
run:
  allow-parallel-runners: true
linters:
  default: none
  enable:
    - copyloopvar
    - dupl
    - errcheck
    - ginkgolinter
    - goconst
    - gocyclo
    - govet
    - ineffassign
    - lll
    - modernize
    - misspell
    - nakedret
    - prealloc
    - revive
    - staticcheck
    - unconvert
    - unparam
    - unused
    - logcheck
  settings:
    custom:
      logcheck:
        type: "module"
        description: Checks Go logging calls for Kubernetes logging conventions.
    revive:
      rules:
        - name: comment-spacings
        - name: import-shadowing
    modernize:
      disable:
        - omitzero
  exclusions:
    generated: lax
    rules:
      - linters:
          - lll
        path: api/*
      - linters:
          - dupl
          - lll
        path: internal/*
    paths:
      - third_party$
      - builtin$
      - examples$
formatters:
  enable:
    - gofmt
    - goimports
  exclusions:
    generated: lax
    paths:
      - third_party$
      - builtin$
      - examples$


================================================
FILE: testdata/project-v4-with-plugins/AGENTS.md
================================================
# project-v4-with-plugins - AI Agent Guide

## Project Structure

**Single-group layout (default):**
```
cmd/main.go                    Manager entry (registers controllers/webhooks)
api//*_types.go       CRD schemas (+kubebuilder markers)
api//zz_generated.*   Auto-generated (DO NOT EDIT)
internal/controller/*          Reconciliation logic
internal/webhook/*             Validation/defaulting (if present)
config/crd/bases/*             Generated CRDs (DO NOT EDIT)
config/rbac/role.yaml          Generated RBAC (DO NOT EDIT)
config/samples/*               Example CRs (edit these)
Makefile                       Build/test/deploy commands
PROJECT                        Kubebuilder metadata Auto-generated (DO NOT EDIT)
```

**Multi-group layout** (for projects with multiple API groups):
```
api///*_types.go       CRD schemas by group
internal/controller//*          Controllers by group
internal/webhook///*   Webhooks by group and version (if present)
```

Multi-group layout organizes APIs by group name (e.g., `batch`, `apps`). Check the `PROJECT` file for `multigroup: true`.

**To convert to multi-group layout:**
1. Run: `kubebuilder edit --multigroup=true`
2. Move APIs: `mkdir -p api/ && mv api/ api//`
3. Move controllers: `mkdir -p internal/controller/ && mv internal/controller/*.go internal/controller//`
4. Move webhooks (if present): `mkdir -p internal/webhook/ && mv internal/webhook/ internal/webhook//`
5. Update import paths in all files
6. Fix `path` in `PROJECT` file for each resource
7. Update test suite CRD paths (add one more `..` to relative paths)

## Critical Rules

### Never Edit These (Auto-Generated)
- `config/crd/bases/*.yaml` - from `make manifests`
- `config/rbac/role.yaml` - from `make manifests`
- `config/webhook/manifests.yaml` - from `make manifests`
- `**/zz_generated.*.go` - from `make generate`
- `PROJECT` - from `kubebuilder [OPTIONS]`

### Never Remove Scaffold Markers
Do NOT delete `// +kubebuilder:scaffold:*` comments. CLI injects code at these markers.

### Keep Project Structure
Do not move files around. The CLI expects files in specific locations.

### Always Use CLI Commands
Always use `kubebuilder create api` and `kubebuilder create webhook` to scaffold. Do NOT create files manually.

### E2E Tests Require an Isolated Kind Cluster
The e2e tests are designed to validate the solution in an isolated environment (similar to GitHub Actions CI).
Ensure you run them against a dedicated [Kind](https://kind.sigs.k8s.io/) cluster (not your “real” dev/prod cluster).

## After Making Changes

**After editing `*_types.go` or markers:**
```
make manifests  # Regenerate CRDs/RBAC from markers
make generate   # Regenerate DeepCopy methods
```

**After editing `*.go` files:**
```
make lint-fix   # Auto-fix code style
make test       # Run unit tests
```

## CLI Commands Cheat Sheet

### Create API (your own types)
```bash
kubebuilder create api --group  --version  --kind 
```

### Deploy Image Plugin (scaffold to deploy/manage ANY container image)

Generate a controller that deploys and manages a container image (nginx, redis, memcached, your app, etc.):

```bash
# Example: deploying memcached
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached \
  --image=memcached:alpine \
  --plugins=deploy-image.go.kubebuilder.io/v1-alpha
```

Scaffolds good-practice code: reconciliation logic, status conditions, finalizers, RBAC. Use as a reference implementation.


### Create Webhooks
```bash
# Validation + defaulting
kubebuilder create webhook --group  --version  --kind  \
  --defaulting --programmatic-validation

# Conversion webhook (for multi-version APIs)
kubebuilder create webhook --group  --version v1 --kind  \
  --conversion --spoke v2
```

### Controller for Core Kubernetes Types
```bash
# Watch Pods
kubebuilder create api --group core --version v1 --kind Pod \
  --controller=true --resource=false

# Watch Deployments
kubebuilder create api --group apps --version v1 --kind Deployment \
  --controller=true --resource=false
```

### Controller for External Types (e.g., from other operators)

Watch resources from external APIs (cert-manager, Argo CD, Istio, etc.):

```bash
# Example: watching cert-manager Certificate resources
kubebuilder create api \
  --group cert-manager --version v1 --kind Certificate \
  --controller=true --resource=false \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
```

**Note:** Use `--external-api-module=@` only if you need a specific version. Otherwise, omit `@` to use what's in go.mod.

### Webhook for External Types

```bash
# Example: validating external resources
kubebuilder create webhook \
  --group cert-manager --version v1 --kind Issuer \
  --defaulting \
  --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
  --external-api-domain=io \
  --external-api-module=github.com/cert-manager/cert-manager
```

## Testing & Development

```bash
make test              # Run unit tests (uses envtest: real K8s API + etcd)
make run               # Run locally (uses current kubeconfig context)
```

Tests use **Ginkgo + Gomega** (BDD style). Check `suite_test.go` for setup.

## Deployment Workflow

```bash
# 1. Regenerate manifests
make manifests generate

# 2. Build & deploy
export IMG=/:tag
make docker-build docker-push IMG=$IMG  # Or: kind load docker-image $IMG --name 
make deploy IMG=$IMG

# 3. Test
kubectl apply -k config/samples/

# 4. Debug
kubectl logs -n -system deployment/-controller-manager -c manager -f
```

### API Design

**Key markers for** `api//*_types.go`:

```go
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=".status.conditions[?(@.type=='Ready')].status"

// On fields:
// +kubebuilder:validation:Required
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:MaxLength=100
// +kubebuilder:validation:Pattern="^[a-z]+$"
// +kubebuilder:default="value"
```

- **Use** `metav1.Condition` for status (not custom string fields)
- **Use predefined types**: `metav1.Time` instead of `string` for dates
- **Follow K8s API conventions**: Standard field names (`spec`, `status`, `metadata`)

### Controller Design

**RBAC markers in** `internal/controller/*_controller.go`:

```go
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=mygroup.example.com,resources=mykinds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
```

**Implementation rules:**
- **Idempotent reconciliation**: Safe to run multiple times
- **Re-fetch before updates**: `r.Get(ctx, req.NamespacedName, obj)` before `r.Update` to avoid conflicts
- **Structured logging**: `log := log.FromContext(ctx); log.Info("msg", "key", val)`
- **Owner references**: Enable automatic garbage collection (`SetControllerReference`)
- **Watch secondary resources**: Use `.Owns()` or `.Watches()`, not just `RequeueAfter`
- **Finalizers**: Clean up external resources (buckets, VMs, DNS entries)

### Logging

**Follow Kubernetes logging message style guidelines:**

- Start from a capital letter
- Do not end the message with a period
- Active voice: subject present (`"Deployment could not create Pod"`) or omitted (`"Could not create Pod"`)
- Past tense: `"Could not delete Pod"` not `"Cannot delete Pod"`
- Specify object type: `"Deleted Pod"` not `"Deleted"`
- Balanced key-value pairs

```go
log.Info("Starting reconciliation")
log.Info("Created Deployment", "name", deploy.Name)
log.Error(err, "Failed to create Pod", "name", name)
```

**Reference:** https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines

### Webhooks
- **Create all types together**: `--defaulting --programmatic-validation --conversion`
- **When`--force`is used**: Backup custom logic first, then restore after scaffolding
- **For multi-version APIs**: Use hub-and-spoke pattern (`--conversion --spoke v2`)
  - Hub version: Usually oldest stable version (v1)
  - Spoke versions: Newer versions that convert to/from hub (v2, v3)
  - Example: `--group crew --version v1 --kind Captain --conversion --spoke v2` (v1 is hub, v2 is spoke)

### Learning from Examples

The **deploy-image plugin** scaffolds a complete controller following good practices. Use it as a reference implementation:

```bash
kubebuilder create api --group example --version v1alpha1 --kind MyApp \
  --image= --plugins=deploy-image.go.kubebuilder.io/v1-alpha
```

Generated code includes: status conditions (`metav1.Condition`), finalizers, owner references, events, idempotent reconciliation.

## Distribution Options

### Option 1: YAML Bundle (Kustomize)

```bash
# Generate dist/install.yaml from Kustomize manifests
make build-installer IMG=/:tag
```

**Key points:**
- The `dist/install.yaml` is generated from Kustomize manifests (CRDs, RBAC, Deployment)
- Commit this file to your repository for easy distribution
- Users only need `kubectl` to install (no additional tools required)

**Example:** Users install with a single command:
```bash
kubectl apply -f https://raw.githubusercontent.com////dist/install.yaml
```

### Option 2: Helm Chart

```bash
kubebuilder edit --plugins=helm/v2-alpha                      # Generates dist/chart/ (default)
kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts  # Generates charts/chart/
```

**For development:**
```bash
make helm-deploy IMG=/:          # Deploy manager via Helm
make helm-deploy IMG=$IMG HELM_EXTRA_ARGS="--set ..."    # Deploy with custom values
make helm-status                                         # Show release status
make helm-uninstall                                      # Remove release
make helm-history                                        # View release history
make helm-rollback                                       # Rollback to previous version
```

**For end users/production:**
```bash
helm install my-release .//chart/ --namespace  --create-namespace
```

**Important:** If you add webhooks or modify manifests after initial chart generation:
1. Backup any customizations in `/chart/values.yaml` and `/chart/manager/manager.yaml`
2. Re-run: `kubebuilder edit --plugins=helm/v2-alpha --force` (use same `--output-dir` if customized)
3. Manually restore your custom values from the backup

### Publish Container Image

```bash
export IMG=/:
make docker-build docker-push IMG=$IMG
```

## References

### Essential Reading
- **Kubebuilder Book**: https://book.kubebuilder.io (comprehensive guide)
- **controller-runtime FAQ**: https://github.com/kubernetes-sigs/controller-runtime/blob/main/FAQ.md (common patterns and questions)
- **Good Practices**: https://book.kubebuilder.io/reference/good-practices.html (why reconciliation is idempotent, status conditions, etc.)
- **Logging Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md#message-style-guidelines (message style, verbosity levels)

### API Design & Implementation
- **API Conventions**: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md
- **Operator Pattern**: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
- **Markers Reference**: https://book.kubebuilder.io/reference/markers.html

### Tools & Libraries
- **controller-runtime**: https://github.com/kubernetes-sigs/controller-runtime
- **controller-tools**: https://github.com/kubernetes-sigs/controller-tools
- **Kubebuilder Repo**: https://github.com/kubernetes-sigs/kubebuilder


================================================
FILE: testdata/project-v4-with-plugins/Dockerfile
================================================
# Build the manager binary
FROM golang:1.25 AS builder
ARG TARGETOS
ARG TARGETARCH

WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# cache deps before building and copying source so that we don't need to re-download as much
# and so that source changes don't invalidate our downloaded layer
RUN go mod download

# Copy the Go source (relies on .dockerignore to filter)
COPY . .

# Build
# the GOARCH has no default value to allow the binary to be built according to the host where the command
# was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore,
# by leaving it empty we can ensure that the container and binary shipped on it will have the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go

# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532

ENTRYPOINT ["/manager"]


================================================
FILE: testdata/project-v4-with-plugins/Makefile
================================================
# Image URL to use all building/pushing image targets
IMG ?= controller:latest

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

# CONTAINER_TOOL defines the container tool to be used for building images.
# Be aware that the target commands are only tested with Docker which is
# scaffolded by default. However, you might want to replace it to use other
# tools. (i.e. podman)
CONTAINER_TOOL ?= docker

# Setting SHELL to bash allows bash commands to be executed by recipes.
# Options are set to exit when a recipe line exits non-zero or a piped command fails.
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec

.PHONY: all
all: build

##@ General

# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk command is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php

.PHONY: help
help: ## Display this help.
	@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n  make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf "  \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Development

.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
	"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases

.PHONY: generate
generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations.
	"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" paths="./..."

.PHONY: fmt
fmt: ## Run go fmt against code.
	go fmt ./...

.PHONY: vet
vet: ## Run go vet against code.
	go vet ./...

.PHONY: test
test: manifests generate fmt vet setup-envtest ## Run tests.
	KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out

# TODO(user): To use a different vendor for e2e tests, modify the setup under 'tests/e2e'.
# The default setup assumes Kind is pre-installed and builds/loads the Manager Docker image locally.
# CertManager is installed by default; skip with:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= project-v4-with-plugins-test-e2e

.PHONY: setup-test-e2e
setup-test-e2e: ## Set up a Kind cluster for e2e tests if it does not exist
	@command -v $(KIND) >/dev/null 2>&1 || { \
		echo "Kind is not installed. Please install Kind manually."; \
		exit 1; \
	}
	@case "$$($(KIND) get clusters)" in \
		*"$(KIND_CLUSTER)"*) \
			echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
		*) \
			echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
			$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
	esac

.PHONY: test-e2e
test-e2e: setup-test-e2e manifests generate fmt vet ## Run the e2e tests. Expected an isolated environment using Kind.
	KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
	$(MAKE) cleanup-test-e2e

.PHONY: cleanup-test-e2e
cleanup-test-e2e: ## Tear down the Kind cluster used for e2e tests
	@$(KIND) delete cluster --name $(KIND_CLUSTER)

.PHONY: lint
lint: golangci-lint ## Run golangci-lint linter
	"$(GOLANGCI_LINT)" run

.PHONY: lint-fix
lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes
	"$(GOLANGCI_LINT)" run --fix

.PHONY: lint-config
lint-config: golangci-lint ## Verify golangci-lint linter configuration
	"$(GOLANGCI_LINT)" config verify

##@ Build

.PHONY: build
build: manifests generate fmt vet ## Build manager binary.
	go build -o bin/manager cmd/main.go

.PHONY: run
run: manifests generate fmt vet ## Run a controller from your host.
	go run ./cmd/main.go

# If you wish to build the manager image targeting other platforms you can use the --platform flag.
# (i.e. docker build --platform linux/arm64). However, you must enable docker buildKit for it.
# More info: https://docs.docker.com/develop/develop-images/build_enhancements/
.PHONY: docker-build
docker-build: ## Build docker image with the manager.
	$(CONTAINER_TOOL) build -t ${IMG} .

.PHONY: docker-push
docker-push: ## Push docker image with the manager.
	$(CONTAINER_TOOL) push ${IMG}

# PLATFORMS defines the target platforms for the manager image be built to provide support to multiple
# architectures. (i.e. make docker-buildx IMG=myregistry/mypoperator:0.0.1). To use this option you need to:
# - be able to use docker buildx. More info: https://docs.docker.com/build/buildx/
# - have enabled BuildKit. More info: https://docs.docker.com/develop/develop-images/build_enhancements/
# - be able to push the image to your registry (i.e. if you do not set a valid value via IMG=> then the export will fail)
# To adequately provide solutions that are compatible with multiple platforms, you should consider using this option.
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: ## Build and push docker image for the manager for cross-platform support
	# copy existing Dockerfile and insert --platform=${BUILDPLATFORM} into Dockerfile.cross, and preserve the original Dockerfile
	sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
	- $(CONTAINER_TOOL) buildx create --name project-v4-with-plugins-builder
	$(CONTAINER_TOOL) buildx use project-v4-with-plugins-builder
	- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) --tag ${IMG} -f Dockerfile.cross .
	- $(CONTAINER_TOOL) buildx rm project-v4-with-plugins-builder
	rm Dockerfile.cross

.PHONY: build-installer
build-installer: manifests generate kustomize ## Generate a consolidated YAML with CRDs and deployment.
	mkdir -p dist
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default > dist/install.yaml

##@ Deployment

ifndef ignore-not-found
  ignore-not-found = false
endif

.PHONY: install
install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~/.kube/config.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi

.PHONY: uninstall
uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
	if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi

.PHONY: deploy
deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
	cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -

.PHONY: undeploy
undeploy: kustomize ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion.
	"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -

##@ Dependencies

## Location to install dependencies to
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
	mkdir -p "$(LOCALBIN)"

## Tool Binaries
KUBECTL ?= kubectl
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint

## Tool Versions
KUSTOMIZE_VERSION ?= v5.8.1
CONTROLLER_TOOLS_VERSION ?= v0.20.1

#ENVTEST_VERSION is the version of controller-runtime release branch to fetch the envtest setup script (i.e. release-0.20)
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_VERSION manually (controller-runtime replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?([0-9]+)\.([0-9]+).*/release-\1.\2/')

#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
  [ -n "$$v" ] || { echo "Set ENVTEST_K8S_VERSION manually (k8s.io/api replace has no tag)" >&2; exit 1; }; \
  printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')

GOLANGCI_LINT_VERSION ?= v2.8.0
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## Download kustomize locally if necessary.
$(KUSTOMIZE): $(LOCALBIN)
	$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))

.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessary.
$(CONTROLLER_GEN): $(LOCALBIN)
	$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))

.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
	@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
	@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
		echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
		exit 1; \
	}

.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
	$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
	$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
	@test -f .custom-gcl.yml && { \
		echo "Building custom golangci-lint with plugins..." && \
		$(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \
		mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \
	} || true

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
# $2 - package url which can be installed
# $3 - specific version of package
define go-install-tool
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f "$(1)" ;\
GOBIN="$(LOCALBIN)" go install $${package} ;\
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
} ;\
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
endef

define gomodver
$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
endef

##@ Helm Deployment

## Helm binary to use for deploying the chart
HELM ?= helm
## Namespace to deploy the Helm release
HELM_NAMESPACE ?= project-v4-with-plugins-system
## Name of the Helm release
HELM_RELEASE ?= project-v4-with-plugins
## Path to the Helm chart directory
HELM_CHART_DIR ?= dist/chart
## Additional arguments to pass to helm commands
HELM_EXTRA_ARGS ?=

.PHONY: install-helm
install-helm: ## Install the latest version of Helm.
	@command -v $(HELM) >/dev/null 2>&1 || { \
		echo "Installing Helm..." && \
		curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
	}

.PHONY: helm-deploy
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
	$(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
		--namespace $(HELM_NAMESPACE) \
		--create-namespace \
		--set manager.image.repository=$${IMG%:*} \
		--set manager.image.tag=$${IMG##*:} \
		--wait \
		--timeout 5m \
		$(HELM_EXTRA_ARGS)

.PHONY: helm-uninstall
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
	$(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-status
helm-status: ## Show Helm release status.
	$(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-history
helm-history: ## Show Helm release history.
	$(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)

.PHONY: helm-rollback
helm-rollback: ## Rollback to previous Helm release.
	$(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)


================================================
FILE: testdata/project-v4-with-plugins/PROJECT
================================================
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
cliVersion: (devel)
domain: testproject.org
layout:
- go.kubebuilder.io/v4
namespaced: true
plugins:
  autoupdate.kubebuilder.io/v1-alpha:
    useGHModels: true
  deploy-image.go.kubebuilder.io/v1-alpha:
    resources:
    - domain: testproject.org
      group: example.com
      kind: Memcached
      options:
        containerCommand: memcached,--memory-limit=64,-o,modern,-v
        containerPort: "11211"
        image: memcached:1.6.26-alpine3.19
        runAsUser: "1001"
      version: v1alpha1
    - domain: testproject.org
      group: example.com
      kind: Busybox
      options:
        image: busybox:1.36.1
      version: v1alpha1
  grafana.kubebuilder.io/v1-alpha: {}
  helm.kubebuilder.io/v2-alpha:
    manifests: dist/install.yaml
    output: dist
projectName: project-v4-with-plugins
repo: sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins
resources:
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: example.com
  kind: Memcached
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1
  version: v1alpha1
  webhooks:
    validation: true
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: example.com
  kind: Busybox
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1
  version: v1alpha1
- api:
    crdVersion: v1
    namespaced: true
  controller: true
  domain: testproject.org
  group: example.com
  kind: Wordpress
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1
  version: v1
  webhooks:
    conversion: true
    spoke:
    - v2
    webhookVersion: v1
- api:
    crdVersion: v1
    namespaced: true
  domain: testproject.org
  group: example.com
  kind: Wordpress
  path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v2
  version: v2
version: "3"


================================================
FILE: testdata/project-v4-with-plugins/README.md
================================================
# project-v4-with-plugins
// TODO(user): Add simple overview of use/purpose

## Description
// TODO(user): An in-depth paragraph about your project and overview of use

## Getting Started

### Prerequisites
- go version v1.24.6+
- docker version 17.03+.
- kubectl version v1.11.3+.
- Access to a Kubernetes v1.11.3+ cluster.

### To Deploy on the cluster
**Build and push your image to the location specified by `IMG`:**

```sh
make docker-build docker-push IMG=/project-v4-with-plugins:tag
```

**NOTE:** This image ought to be published in the personal registry you specified.
And it is required to have access to pull the image from the working environment.
Make sure you have the proper permission to the registry if the above commands don’t work.

**Install the CRDs into the cluster:**

```sh
make install
```

**Deploy the Manager to the cluster with the image specified by `IMG`:**

```sh
make deploy IMG=/project-v4-with-plugins:tag
```

> **NOTE**: If you encounter RBAC errors, you may need to grant yourself cluster-admin
privileges or be logged in as admin.

**Create instances of your solution**
You can apply the samples (examples) from the config/sample:

```sh
kubectl apply -k config/samples/
```

>**NOTE**: Ensure that the samples has default values to test it out.

### To Uninstall
**Delete the instances (CRs) from the cluster:**

```sh
kubectl delete -k config/samples/
```

**Delete the APIs(CRDs) from the cluster:**

```sh
make uninstall
```

**UnDeploy the controller from the cluster:**

```sh
make undeploy
```

## Project Distribution

Following the options to release and provide this solution to the users.

### By providing a bundle with all YAML files

1. Build the installer for the image built and published in the registry:

```sh
make build-installer IMG=/project-v4-with-plugins:tag
```

**NOTE:** The makefile target mentioned above generates an 'install.yaml'
file in the dist directory. This file contains all the resources built
with Kustomize, which are necessary to install this project without its
dependencies.

2. Using the installer

Users can just run 'kubectl apply -f ' to install
the project, i.e.:

```sh
kubectl apply -f https://raw.githubusercontent.com//project-v4-with-plugins//dist/install.yaml
```

### By providing a Helm Chart

1. Build the chart using the optional helm plugin

```sh
kubebuilder edit --plugins=helm/v2-alpha
```

2. See that a chart was generated under 'dist/chart', and users
can obtain this solution from there.

**NOTE:** If you change the project, you need to update the Helm Chart
using the same command above to sync the latest changes. Furthermore,
if you create webhooks, you need to use the above command with
the '--force' flag and manually ensure that any custom configuration
previously added to 'dist/chart/values.yaml' or 'dist/chart/manager/manager.yaml'
is manually re-applied afterwards.

## Contributing
// TODO(user): Add detailed information on how you would like others to contribute to this project

**NOTE:** Run `make help` for more information on all potential `make` targets

More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html)

## License

Copyright 2026 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: testdata/project-v4-with-plugins/api/v1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1 contains API Schema definitions for the example.com v1 API group.
// +kubebuilder:object:generate=true
// +groupName=example.com.testproject.org
package v1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "example.com.testproject.org", Version: "v1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-with-plugins/api/v1/wordpress_conversion.go
================================================
/*
Copyright 2026 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 v1

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// Hub marks this type as a conversion hub.
func (*Wordpress) Hub() {}


================================================
FILE: testdata/project-v4-with-plugins/api/v1/wordpress_types.go
================================================
/*
Copyright 2026 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 v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// WordpressSpec defines the desired state of Wordpress
type WordpressSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Wordpress. Edit wordpress_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// WordpressStatus defines the observed state of Wordpress.
type WordpressStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Wordpress resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status

// Wordpress is the Schema for the wordpresses API
type Wordpress struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Wordpress
	// +required
	Spec WordpressSpec `json:"spec"`

	// status defines the observed state of Wordpress
	// +optional
	Status WordpressStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// WordpressList contains a list of Wordpress
type WordpressList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Wordpress `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Wordpress{}, &WordpressList{})
}


================================================
FILE: testdata/project-v4-with-plugins/api/v1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Wordpress) DeepCopyInto(out *Wordpress) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Wordpress.
func (in *Wordpress) DeepCopy() *Wordpress {
	if in == nil {
		return nil
	}
	out := new(Wordpress)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Wordpress) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressList) DeepCopyInto(out *WordpressList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Wordpress, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressList.
func (in *WordpressList) DeepCopy() *WordpressList {
	if in == nil {
		return nil
	}
	out := new(WordpressList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *WordpressList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressSpec) DeepCopyInto(out *WordpressSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressSpec.
func (in *WordpressSpec) DeepCopy() *WordpressSpec {
	if in == nil {
		return nil
	}
	out := new(WordpressSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressStatus) DeepCopyInto(out *WordpressStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]metav1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressStatus.
func (in *WordpressStatus) DeepCopy() *WordpressStatus {
	if in == nil {
		return nil
	}
	out := new(WordpressStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-with-plugins/api/v1alpha1/busybox_types.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// BusyboxSpec defines the desired state of Busybox
type BusyboxSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// size defines the number of Busybox instances
	// +kubebuilder:default=1
	// +kubebuilder:validation:Minimum=0
	// +optional
	Size *int32 `json:"size,omitempty"`
}

// BusyboxStatus defines the observed state of Busybox
type BusyboxStatus struct {
	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Busybox resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Busybox is the Schema for the busyboxes API
type Busybox struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Busybox
	// +required
	Spec BusyboxSpec `json:"spec"`

	// status defines the observed state of Busybox
	// +optional
	Status BusyboxStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// BusyboxList contains a list of Busybox
type BusyboxList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Busybox `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Busybox{}, &BusyboxList{})
}


================================================
FILE: testdata/project-v4-with-plugins/api/v1alpha1/groupversion_info.go
================================================
/*
Copyright 2026 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 v1alpha1 contains API Schema definitions for the example.com v1alpha1 API group.
// +kubebuilder:object:generate=true
// +groupName=example.com.testproject.org
package v1alpha1

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "example.com.testproject.org", Version: "v1alpha1"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-with-plugins/api/v1alpha1/memcached_types.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// MemcachedSpec defines the desired state of Memcached
type MemcachedSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// size defines the number of Memcached instances
	// +kubebuilder:default=1
	// +kubebuilder:validation:Minimum=0
	// +optional
	Size *int32 `json:"size,omitempty"`

	// containerPort defines the port that will be used to init the container with the image
	// +required
	ContainerPort int32 `json:"containerPort"`
}

// MemcachedStatus defines the observed state of Memcached
type MemcachedStatus struct {
	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Memcached resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Memcached is the Schema for the memcacheds API
type Memcached struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Memcached
	// +required
	Spec MemcachedSpec `json:"spec"`

	// status defines the observed state of Memcached
	// +optional
	Status MemcachedStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// MemcachedList contains a list of Memcached
type MemcachedList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Memcached `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Memcached{}, &MemcachedList{})
}


================================================
FILE: testdata/project-v4-with-plugins/api/v1alpha1/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v1alpha1

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Busybox) DeepCopyInto(out *Busybox) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Busybox.
func (in *Busybox) DeepCopy() *Busybox {
	if in == nil {
		return nil
	}
	out := new(Busybox)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Busybox) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BusyboxList) DeepCopyInto(out *BusyboxList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Busybox, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxList.
func (in *BusyboxList) DeepCopy() *BusyboxList {
	if in == nil {
		return nil
	}
	out := new(BusyboxList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BusyboxList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BusyboxSpec) DeepCopyInto(out *BusyboxSpec) {
	*out = *in
	if in.Size != nil {
		in, out := &in.Size, &out.Size
		*out = new(int32)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxSpec.
func (in *BusyboxSpec) DeepCopy() *BusyboxSpec {
	if in == nil {
		return nil
	}
	out := new(BusyboxSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BusyboxStatus) DeepCopyInto(out *BusyboxStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BusyboxStatus.
func (in *BusyboxStatus) DeepCopy() *BusyboxStatus {
	if in == nil {
		return nil
	}
	out := new(BusyboxStatus)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Memcached) DeepCopyInto(out *Memcached) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Memcached.
func (in *Memcached) DeepCopy() *Memcached {
	if in == nil {
		return nil
	}
	out := new(Memcached)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Memcached) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MemcachedList) DeepCopyInto(out *MemcachedList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Memcached, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedList.
func (in *MemcachedList) DeepCopy() *MemcachedList {
	if in == nil {
		return nil
	}
	out := new(MemcachedList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *MemcachedList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MemcachedSpec) DeepCopyInto(out *MemcachedSpec) {
	*out = *in
	if in.Size != nil {
		in, out := &in.Size, &out.Size
		*out = new(int32)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedSpec.
func (in *MemcachedSpec) DeepCopy() *MemcachedSpec {
	if in == nil {
		return nil
	}
	out := new(MemcachedSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MemcachedStatus) DeepCopyInto(out *MemcachedStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemcachedStatus.
func (in *MemcachedStatus) DeepCopy() *MemcachedStatus {
	if in == nil {
		return nil
	}
	out := new(MemcachedStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-with-plugins/api/v2/groupversion_info.go
================================================
/*
Copyright 2026 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 v2 contains API Schema definitions for the example.com v2 API group.
// +kubebuilder:object:generate=true
// +groupName=example.com.testproject.org
package v2

import (
	"k8s.io/apimachinery/pkg/runtime/schema"
	"sigs.k8s.io/controller-runtime/pkg/scheme"
)

var (
	// SchemeGroupVersion is group version used to register these objects.
	// This name is used by applyconfiguration generators (e.g. controller-gen).
	SchemeGroupVersion = schema.GroupVersion{Group: "example.com.testproject.org", Version: "v2"}

	// GroupVersion is an alias for SchemeGroupVersion, for backward compatibility.
	GroupVersion = SchemeGroupVersion

	// SchemeBuilder is used to add go types to the GroupVersionKind scheme.
	SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion}

	// AddToScheme adds the types in this group-version to the given scheme.
	AddToScheme = SchemeBuilder.AddToScheme
)


================================================
FILE: testdata/project-v4-with-plugins/api/v2/wordpress_conversion.go
================================================
/*
Copyright 2026 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 v2

import (
	"log"

	"sigs.k8s.io/controller-runtime/pkg/conversion"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
)

// ConvertTo converts this Wordpress (v2) to the Hub version (v1).
func (src *Wordpress) ConvertTo(dstRaw conversion.Hub) error {
	dst := dstRaw.(*examplecomv1.Wordpress)
	log.Printf("ConvertTo: Converting Wordpress from Spoke version v2 to Hub version v1;"+
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)

	// TODO(user): Implement conversion logic from v2 to v1
	// Example: Copying Spec fields
	// dst.Spec.Size = src.Spec.Replicas

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}

// ConvertFrom converts the Hub version (v1) to this Wordpress (v2).
func (dst *Wordpress) ConvertFrom(srcRaw conversion.Hub) error {
	src := srcRaw.(*examplecomv1.Wordpress)
	log.Printf("ConvertFrom: Converting Wordpress from Hub version v1 to Spoke version v2;"+
		"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)

	// TODO(user): Implement conversion logic from v1 to v2
	// Example: Copying Spec fields
	// dst.Spec.Replicas = src.Spec.Size

	// Copy ObjectMeta to preserve name, namespace, labels, etc.
	dst.ObjectMeta = src.ObjectMeta

	return nil
}


================================================
FILE: testdata/project-v4-with-plugins/api/v2/wordpress_types.go
================================================
/*
Copyright 2026 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 v2

import (
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// WordpressSpec defines the desired state of Wordpress
type WordpressSpec struct {
	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
	// Important: Run "make" to regenerate code after modifying this file
	// The following markers will use OpenAPI v3 schema to validate the value
	// More info: https://book.kubebuilder.io/reference/markers/crd-validation.html

	// foo is an example field of Wordpress. Edit wordpress_types.go to remove/update
	// +optional
	Foo *string `json:"foo,omitempty"`
}

// WordpressStatus defines the observed state of Wordpress.
type WordpressStatus struct {
	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
	// Important: Run "make" to regenerate code after modifying this file

	// For Kubernetes API conventions, see:
	// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties

	// conditions represent the current state of the Wordpress resource.
	// Each condition has a unique type and reflects the status of a specific aspect of the resource.
	//
	// Standard condition types include:
	// - "Available": the resource is fully functional
	// - "Progressing": the resource is being created or updated
	// - "Degraded": the resource failed to reach or maintain its desired state
	//
	// The status of each condition is one of True, False, or Unknown.
	// +listType=map
	// +listMapKey=type
	// +optional
	Conditions []metav1.Condition `json:"conditions,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:subresource:status

// Wordpress is the Schema for the wordpresses API
type Wordpress struct {
	metav1.TypeMeta `json:",inline"`

	// metadata is a standard object metadata
	// +optional
	metav1.ObjectMeta `json:"metadata,omitzero"`

	// spec defines the desired state of Wordpress
	// +required
	Spec WordpressSpec `json:"spec"`

	// status defines the observed state of Wordpress
	// +optional
	Status WordpressStatus `json:"status,omitzero"`
}

// +kubebuilder:object:root=true

// WordpressList contains a list of Wordpress
type WordpressList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitzero"`
	Items           []Wordpress `json:"items"`
}

func init() {
	SchemeBuilder.Register(&Wordpress{}, &WordpressList{})
}


================================================
FILE: testdata/project-v4-with-plugins/api/v2/zz_generated.deepcopy.go
================================================
//go:build !ignore_autogenerated

/*
Copyright 2026 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 controller-gen. DO NOT EDIT.

package v2

import (
	"k8s.io/apimachinery/pkg/apis/meta/v1"
	runtime "k8s.io/apimachinery/pkg/runtime"
)

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Wordpress) DeepCopyInto(out *Wordpress) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
	in.Spec.DeepCopyInto(&out.Spec)
	in.Status.DeepCopyInto(&out.Status)
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Wordpress.
func (in *Wordpress) DeepCopy() *Wordpress {
	if in == nil {
		return nil
	}
	out := new(Wordpress)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Wordpress) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressList) DeepCopyInto(out *WordpressList) {
	*out = *in
	out.TypeMeta = in.TypeMeta
	in.ListMeta.DeepCopyInto(&out.ListMeta)
	if in.Items != nil {
		in, out := &in.Items, &out.Items
		*out = make([]Wordpress, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressList.
func (in *WordpressList) DeepCopy() *WordpressList {
	if in == nil {
		return nil
	}
	out := new(WordpressList)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *WordpressList) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressSpec) DeepCopyInto(out *WordpressSpec) {
	*out = *in
	if in.Foo != nil {
		in, out := &in.Foo, &out.Foo
		*out = new(string)
		**out = **in
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressSpec.
func (in *WordpressSpec) DeepCopy() *WordpressSpec {
	if in == nil {
		return nil
	}
	out := new(WordpressSpec)
	in.DeepCopyInto(out)
	return out
}

// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *WordpressStatus) DeepCopyInto(out *WordpressStatus) {
	*out = *in
	if in.Conditions != nil {
		in, out := &in.Conditions, &out.Conditions
		*out = make([]v1.Condition, len(*in))
		for i := range *in {
			(*in)[i].DeepCopyInto(&(*out)[i])
		}
	}
}

// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WordpressStatus.
func (in *WordpressStatus) DeepCopy() *WordpressStatus {
	if in == nil {
		return nil
	}
	out := new(WordpressStatus)
	in.DeepCopyInto(out)
	return out
}


================================================
FILE: testdata/project-v4-with-plugins/cmd/main.go
================================================
/*
Copyright 2026 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 main

import (
	"crypto/tls"
	"flag"
	"fmt"
	"os"
	"strings"

	// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
	// to ensure that exec-entrypoint and run can make use of them.
	_ "k8s.io/client-go/plugin/pkg/client/auth"

	"k8s.io/apimachinery/pkg/runtime"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/cache"
	"sigs.k8s.io/controller-runtime/pkg/healthz"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
	examplecomv2 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v2"
	"sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/internal/controller"
	webhookv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/internal/webhook/v1"
	webhookv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/internal/webhook/v1alpha1"
	// +kubebuilder:scaffold:imports
)

var (
	scheme   = runtime.NewScheme()
	setupLog = ctrl.Log.WithName("setup")
)

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(scheme))

	utilruntime.Must(examplecomv1alpha1.AddToScheme(scheme))
	utilruntime.Must(examplecomv1.AddToScheme(scheme))
	utilruntime.Must(examplecomv2.AddToScheme(scheme))
	// +kubebuilder:scaffold:scheme
}

// getWatchNamespace returns the namespace(s) the manager should watch for changes.
// It reads the value from the WATCH_NAMESPACE environment variable.
// - If WATCH_NAMESPACE is not set, an error is returned
// - If WATCH_NAMESPACE contains a single namespace, the manager watches that namespace
// - If WATCH_NAMESPACE contains comma-separated namespaces, the manager watches those namespaces
func getWatchNamespace() (string, error) {
	watchNamespaceEnvVar := "WATCH_NAMESPACE"
	ns, found := os.LookupEnv(watchNamespaceEnvVar)
	if !found {
		return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar)
	}
	return ns, nil
}

// setupCacheNamespaces configures the cache to watch specific namespace(s).
// It supports both single namespace ("ns1") and multi-namespace ("ns1,ns2,ns3") formats.
func setupCacheNamespaces(namespaces string) cache.Options {
	defaultNamespaces := make(map[string]cache.Config)
	for ns := range strings.SplitSeq(namespaces, ",") {
		defaultNamespaces[strings.TrimSpace(ns)] = cache.Config{}
	}
	return cache.Options{
		DefaultNamespaces: defaultNamespaces,
	}
}

// nolint:gocyclo
func main() {
	var metricsAddr string
	var metricsCertPath, metricsCertName, metricsCertKey string
	var webhookCertPath, webhookCertName, webhookCertKey string
	var enableLeaderElection bool
	var probeAddr string
	var secureMetrics bool
	var enableHTTP2 bool
	var tlsOpts []func(*tls.Config)
	flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
		"Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.")
	flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
	flag.BoolVar(&enableLeaderElection, "leader-elect", false,
		"Enable leader election for controller manager. "+
			"Enabling this will ensure there is only one active controller manager.")
	flag.BoolVar(&secureMetrics, "metrics-secure", true,
		"If set, the metrics endpoint is served securely via HTTPS. Use --metrics-secure=false to use HTTP instead.")
	flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
	flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
	flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
	flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
		"The directory that contains the metrics server certificate.")
	flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
	flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
	flag.BoolVar(&enableHTTP2, "enable-http2", false,
		"If set, HTTP/2 will be enabled for the metrics and webhook servers")
	opts := zap.Options{
		Development: true,
	}
	opts.BindFlags(flag.CommandLine)
	flag.Parse()

	ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))

	// if the enable-http2 flag is false (the default), http/2 should be disabled
	// due to its vulnerabilities. More specifically, disabling http/2 will
	// prevent from being vulnerable to the HTTP/2 Stream Cancellation and
	// Rapid Reset CVEs. For more information see:
	// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
	// - https://github.com/advisories/GHSA-4374-p667-p6c8
	disableHTTP2 := func(c *tls.Config) {
		setupLog.Info("Disabling HTTP/2")
		c.NextProtos = []string{"http/1.1"}
	}

	if !enableHTTP2 {
		tlsOpts = append(tlsOpts, disableHTTP2)
	}

	// Initial webhook TLS options
	webhookTLSOpts := tlsOpts
	webhookServerOptions := webhook.Options{
		TLSOpts: webhookTLSOpts,
	}

	if len(webhookCertPath) > 0 {
		setupLog.Info("Initializing webhook certificate watcher using provided certificates",
			"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)

		webhookServerOptions.CertDir = webhookCertPath
		webhookServerOptions.CertName = webhookCertName
		webhookServerOptions.KeyName = webhookCertKey
	}

	webhookServer := webhook.NewServer(webhookServerOptions)

	// Metrics endpoint is enabled in 'config/default/kustomization.yaml'. The Metrics options configure the server.
	// More info:
	// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/server
	// - https://book.kubebuilder.io/reference/metrics.html
	metricsServerOptions := metricsserver.Options{
		BindAddress:   metricsAddr,
		SecureServing: secureMetrics,
		TLSOpts:       tlsOpts,
	}

	if secureMetrics {
		// FilterProvider is used to protect the metrics endpoint with authn/authz.
		// These configurations ensure that only authorized users and service accounts
		// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
		// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/metrics/filters#WithAuthenticationAndAuthorization
		metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
	}

	// If the certificate is not specified, controller-runtime will automatically
	// generate self-signed certificates for the metrics server. While convenient for development and testing,
	// this setup is not recommended for production.
	//
	// TODO(user): If you enable certManager, uncomment the following lines:
	// - [METRICS-WITH-CERTS] at config/default/kustomization.yaml to generate and use certificates
	// managed by cert-manager for the metrics server.
	// - [PROMETHEUS-WITH-CERTS] at config/prometheus/kustomization.yaml for TLS certification.
	if len(metricsCertPath) > 0 {
		setupLog.Info("Initializing metrics certificate watcher using provided certificates",
			"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)

		metricsServerOptions.CertDir = metricsCertPath
		metricsServerOptions.CertName = metricsCertName
		metricsServerOptions.KeyName = metricsCertKey
	}

	// Get the namespace(s) for namespace-scoped mode from WATCH_NAMESPACE environment variable.
	// The manager will only watch and manage resources in the specified namespace(s).
	watchNamespace, err := getWatchNamespace()
	if err != nil {
		setupLog.Error(err, "Unable to get WATCH_NAMESPACE, "+
			"the manager will watch and manage resources in all namespaces")
		os.Exit(1)
	}

	// Configure manager options for namespace-scoped mode
	mgrOptions := ctrl.Options{
		Scheme:                 scheme,
		Metrics:                metricsServerOptions,
		WebhookServer:          webhookServer,
		HealthProbeBindAddress: probeAddr,
		LeaderElection:         enableLeaderElection,
		LeaderElectionID:       "a13ae5d8.testproject.org",
		// LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily
		// when the Manager ends. This requires the binary to immediately end when the
		// Manager is stopped, otherwise, this setting is unsafe. Setting this significantly
		// speeds up voluntary leader transitions as the new leader don't have to wait
		// LeaseDuration time first.
		//
		// In the default scaffold provided, the program ends immediately after
		// the manager stops, so would be fine to enable this option. However,
		// if you are doing or is intended to do any operation such as perform cleanups
		// after the manager stops then its usage might be unsafe.
		// LeaderElectionReleaseOnCancel: true,
	}

	// Configure cache to watch namespace(s) specified in WATCH_NAMESPACE
	mgrOptions.Cache = setupCacheNamespaces(watchNamespace)
	setupLog.Info("Watching namespace(s)", "namespaces", watchNamespace)

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions)
	if err != nil {
		setupLog.Error(err, "Failed to start manager")
		os.Exit(1)
	}

	if err := (&controller.MemcachedReconciler{
		Client:   mgr.GetClient(),
		Scheme:   mgr.GetScheme(),
		Recorder: mgr.GetEventRecorder("memcached-controller"),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Memcached")
		os.Exit(1)
	}
	if err := (&controller.BusyboxReconciler{
		Client:   mgr.GetClient(),
		Scheme:   mgr.GetScheme(),
		Recorder: mgr.GetEventRecorder("busybox-controller"),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Busybox")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1alpha1.SetupMemcachedWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Memcached")
			os.Exit(1)
		}
	}
	if err := (&controller.WordpressReconciler{
		Client: mgr.GetClient(),
		Scheme: mgr.GetScheme(),
	}).SetupWithManager(mgr); err != nil {
		setupLog.Error(err, "Failed to create controller", "controller", "Wordpress")
		os.Exit(1)
	}
	// nolint:goconst
	if os.Getenv("ENABLE_WEBHOOKS") != "false" {
		if err := webhookv1.SetupWordpressWebhookWithManager(mgr); err != nil {
			setupLog.Error(err, "Failed to create webhook", "webhook", "Wordpress")
			os.Exit(1)
		}
	}
	// +kubebuilder:scaffold:builder

	if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up health check")
		os.Exit(1)
	}
	if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
		setupLog.Error(err, "Failed to set up ready check")
		os.Exit(1)
	}

	setupLog.Info("Starting manager")
	if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
		setupLog.Error(err, "Failed to run manager")
		os.Exit(1)
	}
}


================================================
FILE: testdata/project-v4-with-plugins/config/certmanager/certificate-metrics.yaml
================================================
# The following manifests contain a self-signed issuer CR and a metrics certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: metrics-certs  # this name should match the one appeared in kustomizeconfig.yaml
  namespace: system
spec:
  dnsNames:
  # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
  # replacements in the config/default/kustomization.yaml file.
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: metrics-server-cert


================================================
FILE: testdata/project-v4-with-plugins/config/certmanager/certificate-webhook.yaml
================================================
# The following manifests contain a self-signed issuer CR and a certificate CR.
# More document can be found at https://docs.cert-manager.io
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: serving-cert  # this name should match the one appeared in kustomizeconfig.yaml
  namespace: system
spec:
  # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
  # replacements in the config/default/kustomization.yaml file.
  dnsNames:
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: selfsigned-issuer
  secretName: webhook-server-cert


================================================
FILE: testdata/project-v4-with-plugins/config/certmanager/issuer.yaml
================================================
# The following manifest contains a self-signed issuer CR.
# More information can be found at https://docs.cert-manager.io
# WARNING: Targets CertManager v1.0. Check https://cert-manager.io/docs/installation/upgrading/ for breaking changes.
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: selfsigned-issuer
  namespace: system
spec:
  selfSigned: {}


================================================
FILE: testdata/project-v4-with-plugins/config/certmanager/kustomization.yaml
================================================
resources:
- issuer.yaml
- certificate-webhook.yaml
- certificate-metrics.yaml

configurations:
- kustomizeconfig.yaml


================================================
FILE: testdata/project-v4-with-plugins/config/certmanager/kustomizeconfig.yaml
================================================
# This configuration is for teaching kustomize how to update name ref substitution
nameReference:
- kind: Issuer
  group: cert-manager.io
  fieldSpecs:
  - kind: Certificate
    group: cert-manager.io
    path: spec/issuerRef/name


================================================
FILE: testdata/project-v4-with-plugins/config/crd/bases/example.com.testproject.org_busyboxes.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: busyboxes.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Busybox
    listKind: BusyboxList
    plural: busyboxes
    singular: busybox
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Busybox is the Schema for the busyboxes API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Busybox
            properties:
              size:
                default: 1
                description: size defines the number of Busybox instances
                format: int32
                minimum: 0
                type: integer
            type: object
          status:
            description: status defines the observed state of Busybox
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Busybox resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-with-plugins/config/crd/bases/example.com.testproject.org_memcacheds.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: memcacheds.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Memcached
    listKind: MemcachedList
    plural: memcacheds
    singular: memcached
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Memcached is the Schema for the memcacheds API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Memcached
            properties:
              containerPort:
                description: containerPort defines the port that will be used to init
                  the container with the image
                format: int32
                type: integer
              size:
                default: 1
                description: size defines the number of Memcached instances
                format: int32
                minimum: 0
                type: integer
            required:
            - containerPort
            type: object
          status:
            description: status defines the observed state of Memcached
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Memcached resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-with-plugins/config/crd/bases/example.com.testproject.org_wordpresses.yaml
================================================
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: wordpresses.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Wordpress
    listKind: WordpressList
    plural: wordpresses
    singular: wordpress
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
  - name: v2
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: false
    subresources:
      status: {}


================================================
FILE: testdata/project-v4-with-plugins/config/crd/kustomization.yaml
================================================
# This kustomization.yaml is not intended to be run by itself,
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/example.com.testproject.org_memcacheds.yaml
- bases/example.com.testproject.org_busyboxes.yaml
- bases/example.com.testproject.org_wordpresses.yaml
# +kubebuilder:scaffold:crdkustomizeresource

patches:
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
# patches here are for enabling the conversion webhook for each CRD
- path: patches/webhook_in_wordpresses.yaml
# +kubebuilder:scaffold:crdkustomizewebhookpatch

# [WEBHOOK] To enable webhook, uncomment the following section
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml


================================================
FILE: testdata/project-v4-with-plugins/config/crd/kustomizeconfig.yaml
================================================
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
nameReference:
- kind: Service
  version: v1
  fieldSpecs:
  - kind: CustomResourceDefinition
    version: v1
    group: apiextensions.k8s.io
    path: spec/conversion/webhook/clientConfig/service/name

varReference:
- path: metadata/annotations


================================================
FILE: testdata/project-v4-with-plugins/config/crd/patches/webhook_in_wordpresses.yaml
================================================
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: wordpresses.example.com.testproject.org
spec:
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        service:
          namespace: system
          name: webhook-service
          path: /convert
      conversionReviewVersions:
      - v1


================================================
FILE: testdata/project-v4-with-plugins/config/default/cert_metrics_manager_patch.yaml
================================================
# This patch adds the args, volumes, and ports to allow the manager to use the metrics-server certs.

# Add the volumeMount for the metrics-server certs
- op: add
  path: /spec/template/spec/containers/0/volumeMounts/-
  value:
    mountPath: /tmp/k8s-metrics-server/metrics-certs
    name: metrics-certs
    readOnly: true

# Add the --metrics-cert-path argument for the metrics server
- op: add
  path: /spec/template/spec/containers/0/args/-
  value: --metrics-cert-path=/tmp/k8s-metrics-server/metrics-certs

# Add the metrics-server certs volume configuration
- op: add
  path: /spec/template/spec/volumes/-
  value:
    name: metrics-certs
    secret:
      secretName: metrics-server-cert
      optional: false
      items:
        - key: ca.crt
          path: ca.crt
        - key: tls.crt
          path: tls.crt
        - key: tls.key
          path: tls.key


================================================
FILE: testdata/project-v4-with-plugins/config/default/kustomization.yaml
================================================
# Adds namespace to all resources.
namespace: project-v4-with-plugins-system

# Value of this field is prepended to the
# names of all resources, e.g. a deployment named
# "wordpress" becomes "alices-wordpress".
# Note that it should also match with the prefix (text before '-') of the namespace
# field above.
namePrefix: project-v4-with-plugins-

# Labels to add to all resources and selectors.
#labels:
#- includeSelectors: true
#  pairs:
#    someName: someValue

resources:
- ../crd
- ../rbac
- ../manager
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required.
- ../certmanager
# [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'.
#- ../prometheus
# [METRICS] Expose the controller manager metrics service.
- metrics_service.yaml
# [NETWORK POLICY] Protect the /metrics endpoint and Webhook Server with NetworkPolicy.
# Only Pod(s) running a namespace labeled with 'metrics: enabled' will be able to gather the metrics.
# Only CR(s) which requires webhooks and are applied on namespaces labeled with 'webhooks: enabled' will
# be able to communicate with the Webhook Server.
#- ../network-policy

# Uncomment the patches line if you enable Metrics
patches:
# [METRICS] The following patch will enable the metrics endpoint using HTTPS and the port :8443.
# More info: https://book.kubebuilder.io/reference/metrics
- path: manager_metrics_patch.yaml
  target:
    kind: Deployment

# Uncomment the patches line if you enable Metrics and CertManager
# [METRICS-WITH-CERTS] To enable metrics protected with certManager, uncomment the following line.
# This patch will protect the metrics with certManager self-signed certs.
#- path: cert_metrics_manager_patch.yaml
#  target:
#    kind: Deployment

# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in
# crd/kustomization.yaml
- path: manager_webhook_patch.yaml
  target:
    kind: Deployment

# [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix.
# Uncomment the following replacements to add the cert-manager CA injection annotations
replacements:
# - source: # Uncomment the following block to enable certificates for metrics
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.name
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 0
#         create: true
#     - select: # Uncomment the following to set the Service name for TLS config in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 0
#         create: true

# - source:
#     kind: Service
#     version: v1
#     name: controller-manager-metrics-service
#     fieldPath: metadata.namespace
#   targets:
#     - select:
#         kind: Certificate
#         group: cert-manager.io
#         version: v1
#         name: metrics-certs
#       fieldPaths:
#         - spec.dnsNames.0
#         - spec.dnsNames.1
#       options:
#         delimiter: '.'
#         index: 1
#         create: true
#     - select: # Uncomment the following to set the Service namespace for TLS in Prometheus ServiceMonitor
#         kind: ServiceMonitor
#         group: monitoring.coreos.com
#         version: v1
#         name: controller-manager-metrics-monitor
#       fieldPaths:
#         - spec.endpoints.0.tlsConfig.serverName
#       options:
#         delimiter: '.'
#         index: 1
#         create: true

 - source: # Uncomment the following block if you have any webhook
     kind: Service
     version: v1
     name: webhook-service
     fieldPath: .metadata.name # Name of the service
   targets:
     - select:
         kind: Certificate
         group: cert-manager.io
         version: v1
         name: serving-cert
       fieldPaths:
         - .spec.dnsNames.0
         - .spec.dnsNames.1
       options:
         delimiter: '.'
         index: 0
         create: true
 - source:
     kind: Service
     version: v1
     name: webhook-service
     fieldPath: .metadata.namespace # Namespace of the service
   targets:
     - select:
         kind: Certificate
         group: cert-manager.io
         version: v1
         name: serving-cert
       fieldPaths:
         - .spec.dnsNames.0
         - .spec.dnsNames.1
       options:
         delimiter: '.'
         index: 1
         create: true

 - source: # Uncomment the following block if you have a ValidatingWebhook (--programmatic-validation)
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert # This name should match the one in certificate.yaml
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets:
     - select:
         kind: ValidatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets:
     - select:
         kind: ValidatingWebhookConfiguration
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true

# - source: # Uncomment the following block if you have a DefaultingWebhook (--defaulting )
#     kind: Certificate
#     group: cert-manager.io
#     version: v1
#     name: serving-cert
#     fieldPath: .metadata.namespace # Namespace of the certificate CR
#   targets:
#     - select:
#         kind: MutatingWebhookConfiguration
#       fieldPaths:
#         - .metadata.annotations.[cert-manager.io/inject-ca-from]
#       options:
#         delimiter: '/'
#         index: 0
#         create: true
# - source:
#     kind: Certificate
#     group: cert-manager.io
#     version: v1
#     name: serving-cert
#     fieldPath: .metadata.name
#   targets:
#     - select:
#         kind: MutatingWebhookConfiguration
#       fieldPaths:
#         - .metadata.annotations.[cert-manager.io/inject-ca-from]
#       options:
#         delimiter: '/'
#         index: 1
#         create: true

 - source: # Uncomment the following block if you have a ConversionWebhook (--conversion)
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.namespace # Namespace of the certificate CR
   targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
     - select:
         kind: CustomResourceDefinition
         name: wordpresses.example.com.testproject.org
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 0
         create: true
# +kubebuilder:scaffold:crdkustomizecainjectionns
 - source:
     kind: Certificate
     group: cert-manager.io
     version: v1
     name: serving-cert
     fieldPath: .metadata.name
   targets: # Do not remove or uncomment the following scaffold marker; required to generate code for target CRD.
     - select:
         kind: CustomResourceDefinition
         name: wordpresses.example.com.testproject.org
       fieldPaths:
         - .metadata.annotations.[cert-manager.io/inject-ca-from]
       options:
         delimiter: '/'
         index: 1
         create: true
# +kubebuilder:scaffold:crdkustomizecainjectionname


================================================
FILE: testdata/project-v4-with-plugins/config/default/manager_metrics_patch.yaml
================================================
# This patch adds the args to allow exposing the metrics endpoint using HTTPS
- op: add
  path: /spec/template/spec/containers/0/args/0
  value: --metrics-bind-address=:8443


================================================
FILE: testdata/project-v4-with-plugins/config/default/manager_webhook_patch.yaml
================================================
# This patch ensures the webhook certificates are properly mounted in the manager container.
# It configures the necessary arguments, volumes, volume mounts, and container ports.

# Add the --webhook-cert-path argument for configuring the webhook certificate path
- op: add
  path: /spec/template/spec/containers/0/args/-
  value: --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs

# Add the volumeMount for the webhook certificates
- op: add
  path: /spec/template/spec/containers/0/volumeMounts/-
  value:
    mountPath: /tmp/k8s-webhook-server/serving-certs
    name: webhook-certs
    readOnly: true

# Add the port configuration for the webhook server
- op: add
  path: /spec/template/spec/containers/0/ports/-
  value:
    containerPort: 9443
    name: webhook-server
    protocol: TCP

# Add the volume configuration for the webhook certificates
- op: add
  path: /spec/template/spec/volumes/-
  value:
    name: webhook-certs
    secret:
      secretName: webhook-server-cert


================================================
FILE: testdata/project-v4-with-plugins/config/default/metrics_service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager-metrics-service
  namespace: system
spec:
  ports:
  - name: https
    port: 8443
    protocol: TCP
    targetPort: 8443
  selector:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-with-plugins


================================================
FILE: testdata/project-v4-with-plugins/config/manager/kustomization.yaml
================================================
resources:
- manager.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
  newName: controller
  newTag: latest


================================================
FILE: testdata/project-v4-with-plugins/config/manager/manager.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: system
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: controller-manager
  namespace: system
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
spec:
  selector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-with-plugins
  replicas: 1
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        control-plane: controller-manager
        app.kubernetes.io/name: project-v4-with-plugins
    spec:
      # TODO(user): Uncomment the following code to configure the nodeAffinity expression
      # according to the platforms which are supported by your solution.
      # It is considered best practice to support multiple architectures. You can
      # build your manager image using the makefile target docker-buildx.
      # affinity:
      #   nodeAffinity:
      #     requiredDuringSchedulingIgnoredDuringExecution:
      #       nodeSelectorTerms:
      #         - matchExpressions:
      #           - key: kubernetes.io/arch
      #             operator: In
      #             values:
      #               - amd64
      #               - arm64
      #               - ppc64le
      #               - s390x
      #           - key: kubernetes.io/os
      #             operator: In
      #             values:
      #               - linux
      securityContext:
        # Projects are configured by default to adhere to the "restricted" Pod Security Standards.
        # This ensures that deployments meet the highest security requirements for Kubernetes.
        # For more details, see: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      containers:
      - command:
        - /manager
        args:
          - --leader-elect
          - --health-probe-bind-address=:8081
        image: controller:latest
        name: manager
        env:
        - name: BUSYBOX_IMAGE
          value: busybox:1.36.1
        - name: MEMCACHED_IMAGE
          value: memcached:1.6.26-alpine3.19
        - name: WATCH_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        ports: []
        securityContext:
          readOnlyRootFilesystem: true
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - "ALL"
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 15
          periodSeconds: 20
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081
          initialDelaySeconds: 5
          periodSeconds: 10
        # TODO(user): Configure the resources accordingly based on the project requirements.
        # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        volumeMounts: []
      volumes: []
      serviceAccountName: controller-manager
      terminationGracePeriodSeconds: 10


================================================
FILE: testdata/project-v4-with-plugins/config/network-policy/allow-metrics-traffic.yaml
================================================
# This NetworkPolicy allows ingress traffic
# with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those
# namespaces are able to gather data from the metrics endpoint.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: allow-metrics-traffic
  namespace: system
spec:
  podSelector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-with-plugins
  policyTypes:
    - Ingress
  ingress:
    # This allows ingress traffic from any namespace with the label metrics: enabled
    - from:
      - namespaceSelector:
          matchLabels:
            metrics: enabled  # Only from namespaces with this label
      ports:
        - port: 8443
          protocol: TCP


================================================
FILE: testdata/project-v4-with-plugins/config/network-policy/allow-webhook-traffic.yaml
================================================
# This NetworkPolicy allows ingress traffic to your webhook server running
# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks
# will only work when applied in namespaces labeled with 'webhook: enabled'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: allow-webhook-traffic
  namespace: system
spec:
  podSelector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-with-plugins
  policyTypes:
    - Ingress
  ingress:
    # This allows ingress traffic from any namespace with the label webhook: enabled
    - from:
      - namespaceSelector:
          matchLabels:
            webhook: enabled # Only from namespaces with this label
      ports:
        - port: 443
          protocol: TCP


================================================
FILE: testdata/project-v4-with-plugins/config/network-policy/kustomization.yaml
================================================
resources:
- allow-webhook-traffic.yaml
- allow-metrics-traffic.yaml


================================================
FILE: testdata/project-v4-with-plugins/config/prometheus/kustomization.yaml
================================================
resources:
- monitor.yaml

# [PROMETHEUS-WITH-CERTS] The following patch configures the ServiceMonitor in ../prometheus
# to securely reference certificates created and managed by cert-manager.
# Additionally, ensure that you uncomment the [METRICS WITH CERTMANAGER] patch under config/default/kustomization.yaml
# to mount the "metrics-server-cert" secret in the Manager Deployment.
#patches:
#  - path: monitor_tls_patch.yaml
#    target:
#      kind: ServiceMonitor


================================================
FILE: testdata/project-v4-with-plugins/config/prometheus/monitor.yaml
================================================
# Prometheus Monitor Service (Metrics)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager-metrics-monitor
  namespace: system
spec:
  endpoints:
    - path: /metrics
      port: https # Ensure this is the name of the port that exposes HTTPS metrics
      scheme: https
      bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
      tlsConfig:
        # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables
        # certificate verification, exposing the system to potential man-in-the-middle attacks.
        # For production environments, it is recommended to use cert-manager for automatic TLS certificate management.
        # To apply this configuration, enable cert-manager and use the patch located at config/prometheus/servicemonitor_tls_patch.yaml,
        # which securely references the certificate from the 'metrics-server-cert' secret.
        insecureSkipVerify: true
  selector:
    matchLabels:
      control-plane: controller-manager
      app.kubernetes.io/name: project-v4-with-plugins


================================================
FILE: testdata/project-v4-with-plugins/config/prometheus/monitor_tls_patch.yaml
================================================
# Patch for Prometheus ServiceMonitor to enable secure TLS configuration
# using certificates managed by cert-manager
- op: replace
  path: /spec/endpoints/0/tlsConfig
  value:
    # SERVICE_NAME and SERVICE_NAMESPACE will be substituted by kustomize
    serverName: SERVICE_NAME.SERVICE_NAMESPACE.svc
    insecureSkipVerify: false
    ca:
      secret:
        name: metrics-server-cert
        key: ca.crt
    cert:
      secret:
        name: metrics-server-cert
        key: tls.crt
    keySecret:
      name: metrics-server-cert
      key: tls.key


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/busybox_admin_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over example.com.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: busybox-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/busybox_editor_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the example.com.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: busybox-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/busybox_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to example.com.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: busybox-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/kustomization.yaml
================================================
resources:
# All RBAC will be applied under this service account in
# the deployment namespace. You may comment out this resource
# if your manager will use a service account that exists at
# runtime. Be sure to update RoleBinding and ClusterRoleBinding
# subjects if changing service account names.
- service_account.yaml
- role.yaml
- role_binding.yaml
- leader_election_role.yaml
- leader_election_role_binding.yaml
# The following RBAC configurations are used to protect
# the metrics endpoint with authn/authz. These configurations
# ensure that only authorized users and service accounts
# can access the metrics endpoint. Comment the following
# permissions if you want to disable this protection.
# More info: https://book.kubebuilder.io/reference/metrics.html
- metrics_auth_role.yaml
- metrics_auth_role_binding.yaml
- metrics_reader_role.yaml
# For each CRD, "Admin", "Editor" and "Viewer" roles are scaffolded by
# default, aiding admins in cluster management. Those roles are
# not used by the project-v4-with-plugins itself. You can comment the following lines
# if you do not want those helpers be installed with your Project.
- wordpress_admin_role.yaml
- wordpress_editor_role.yaml
- wordpress_viewer_role.yaml
- busybox_admin_role.yaml
- busybox_editor_role.yaml
- busybox_viewer_role.yaml
- memcached_admin_role.yaml
- memcached_editor_role.yaml
- memcached_viewer_role.yaml



================================================
FILE: testdata/project-v4-with-plugins/config/rbac/leader_election_role.yaml
================================================
# permissions to do leader election.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: leader-election-role
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/leader_election_role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: leader-election-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: leader-election-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/memcached_admin_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over example.com.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: memcached-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/memcached_editor_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the example.com.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: memcached-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/memcached_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to example.com.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: memcached-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/metrics_auth_role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: metrics-auth-role
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/metrics_auth_role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: metrics-auth-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: metrics-auth-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/metrics_reader_role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: metrics-reader
rules:
- nonResourceURLs:
  - "/metrics"
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/role.yaml
================================================
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: manager-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - events.k8s.io
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  - memcacheds
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/finalizers
  - memcacheds/finalizers
  - wordpresses/finalizers
  verbs:
  - update
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  - memcacheds/status
  - wordpresses/status
  verbs:
  - get
  - patch
  - update


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/role_binding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: manager-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: manager-role
subjects:
- kind: ServiceAccount
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/service_account.yaml
================================================
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: controller-manager
  namespace: system


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/wordpress_admin_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants full permissions ('*') over example.com.testproject.org.
# This role is intended for users authorized to modify roles and bindings within the cluster,
# enabling them to delegate specific permissions to other users or groups as needed.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: wordpress-admin-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/wordpress_editor_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants permissions to create, update, and delete resources within the example.com.testproject.org.
# This role is intended for users who need to manage these resources
# but should not control RBAC or manage permissions for others.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: wordpress-editor-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/rbac/wordpress_viewer_role.yaml
================================================
# This rule is not used by the project project-v4-with-plugins itself.
# It is provided to allow the cluster admin to help manage permissions for users.
#
# Grants read-only access to example.com.testproject.org resources.
# This role is intended for users who need visibility into these resources
# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: wordpress-viewer-role
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get


================================================
FILE: testdata/project-v4-with-plugins/config/samples/example.com_v1_wordpress.yaml
================================================
apiVersion: example.com.testproject.org/v1
kind: Wordpress
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: wordpress-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-with-plugins/config/samples/example.com_v1alpha1_busybox.yaml
================================================
apiVersion: example.com.testproject.org/v1alpha1
kind: Busybox
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: busybox-sample
spec:
  # TODO(user): edit the following value to ensure the number
  # of Pods/Instances your Operand must have on cluster
  size: 1


================================================
FILE: testdata/project-v4-with-plugins/config/samples/example.com_v1alpha1_memcached.yaml
================================================
apiVersion: example.com.testproject.org/v1alpha1
kind: Memcached
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: memcached-sample
spec:
  # TODO(user): edit the following value to ensure the number
  # of Pods/Instances your Operand must have on cluster
  size: 1

  # TODO(user): edit the following value to ensure the container has the right port to be initialized
  containerPort: 11211


================================================
FILE: testdata/project-v4-with-plugins/config/samples/example.com_v2_wordpress.yaml
================================================
apiVersion: example.com.testproject.org/v2
kind: Wordpress
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: wordpress-sample
spec:
  # TODO(user): Add fields here


================================================
FILE: testdata/project-v4-with-plugins/config/samples/kustomization.yaml
================================================
## Append samples of your project ##
resources:
- example.com_v1alpha1_memcached.yaml
- example.com_v1alpha1_busybox.yaml
- example.com_v1_wordpress.yaml
- example.com_v2_wordpress.yaml
# +kubebuilder:scaffold:manifestskustomizesamples


================================================
FILE: testdata/project-v4-with-plugins/config/webhook/kustomization.yaml
================================================
resources:
- manifests.yaml
- service.yaml


================================================
FILE: testdata/project-v4-with-plugins/config/webhook/manifests.yaml
================================================
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: webhook-service
      namespace: system
      path: /validate-example-com-testproject-org-v1alpha1-memcached
  failurePolicy: Fail
  name: vmemcached-v1alpha1.kb.io
  rules:
  - apiGroups:
    - example.com.testproject.org
    apiVersions:
    - v1alpha1
    operations:
    - CREATE
    - UPDATE
    resources:
    - memcacheds
  sideEffects: None


================================================
FILE: testdata/project-v4-with-plugins/config/webhook/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/name: project-v4-with-plugins
    app.kubernetes.io/managed-by: kustomize
  name: webhook-service
  namespace: system
spec:
  ports:
    - port: 443
      protocol: TCP
      targetPort: 9443
  selector:
    control-plane: controller-manager
    app.kubernetes.io/name: project-v4-with-plugins


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/.helmignore
================================================
# Patterns to ignore when building Helm packages.
# Operating system files
.DS_Store

# Version control directories
.git/
.gitignore
.bzr/
.hg/
.hgignore
.svn/

# Backup and temporary files
*.swp
*.tmp
*.bak
*.orig
*~

# IDE and editor-related files
.idea/
.vscode/

# Helm chart artifacts
dist/chart/*.tgz


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/Chart.yaml
================================================
apiVersion: v2
name: project-v4-with-plugins
description: A Helm chart to distribute project-v4-with-plugins
type: application

version: 0.1.0
appVersion: "0.1.0"

keywords:
  - kubernetes
  - operator

annotations:
  kubebuilder.io/generated-by: kubebuilder


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/NOTES.txt
================================================
Thank you for installing {{ .Chart.Name }}.

Your release is named {{ .Release.Name }}.

The controller and CRDs have been installed in namespace {{ .Release.Namespace }}.

To verify the installation:

  kubectl get pods -n {{ .Release.Namespace }}
  kubectl get customresourcedefinitions

To learn more about the release, try:

  $ helm status {{ .Release.Name }} -n {{ .Release.Namespace }}
  $ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/_helpers.tpl
================================================
{{/*
Expand the name of the chart.
*/}}
{{- define "project-v4-with-plugins.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "project-v4-with-plugins.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Namespace for generated references.
Always uses the Helm release namespace.
*/}}
{{- define "project-v4-with-plugins.namespaceName" -}}
{{- .Release.Namespace }}
{{- end }}

{{/*
Resource name with proper truncation for Kubernetes 63-character limit.
Takes a dict with:
  - .suffix: Resource name suffix (e.g., "metrics", "webhook")
  - .context: Template context (root context with .Values, .Release, etc.)
Dynamically calculates safe truncation to ensure total name length <= 63 chars.
*/}}
{{- define "project-v4-with-plugins.resourceName" -}}
{{- $fullname := include "project-v4-with-plugins.fullname" .context }}
{{- $suffix := .suffix }}
{{- $maxLen := sub 62 (len $suffix) | int }}
{{- if gt (len $fullname) $maxLen }}
{{- printf "%s-%s" (trunc $maxLen $fullname | trimSuffix "-") $suffix | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" $fullname $suffix | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/cert-manager/metrics-certs.yaml
================================================
{{- if and .Values.certManager.enable .Values.metrics.enable }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "metrics-certs" "context" $) }}
  namespace: {{ .Release.Namespace }}
spec:
  dnsNames:
  - {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ include "project-v4-with-plugins.namespaceName" $ }}.svc
  - {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ include "project-v4-with-plugins.namespaceName" $ }}.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }}
  secretName: metrics-server-cert
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/cert-manager/selfsigned-issuer.yaml
================================================
{{- if .Values.certManager.enable }}
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }}
  namespace: {{ .Release.Namespace }}
spec:
  selfSigned: {}
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/cert-manager/serving-cert.yaml
================================================
{{- if .Values.certManager.enable }}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "serving-cert" "context" $) }}
  namespace: {{ .Release.Namespace }}
spec:
  dnsNames:
  - {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc
  - {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "webhook-service" "context" $) }}.{{ .Release.Namespace }}.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "selfsigned-issuer" "context" $) }}
  secretName: webhook-server-cert
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/crd/busyboxes.example.com.testproject.org.yaml
================================================
{{- if .Values.crd.enable }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    {{- if .Values.crd.keep }}
    "helm.sh/resource-policy": keep
    {{- end }}
    controller-gen.kubebuilder.io/version: v0.20.1
  name: busyboxes.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Busybox
    listKind: BusyboxList
    plural: busyboxes
    singular: busybox
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Busybox is the Schema for the busyboxes API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Busybox
            properties:
              size:
                default: 1
                description: size defines the number of Busybox instances
                format: int32
                minimum: 0
                type: integer
            type: object
          status:
            description: status defines the observed state of Busybox
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Busybox resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/crd/memcacheds.example.com.testproject.org.yaml
================================================
{{- if .Values.crd.enable }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    {{- if .Values.crd.keep }}
    "helm.sh/resource-policy": keep
    {{- end }}
    controller-gen.kubebuilder.io/version: v0.20.1
  name: memcacheds.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Memcached
    listKind: MemcachedList
    plural: memcacheds
    singular: memcached
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Memcached is the Schema for the memcacheds API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Memcached
            properties:
              containerPort:
                description: containerPort defines the port that will be used to init
                  the container with the image
                format: int32
                type: integer
              size:
                default: 1
                description: size defines the number of Memcached instances
                format: int32
                minimum: 0
                type: integer
            required:
            - containerPort
            type: object
          status:
            description: status defines the observed state of Memcached
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Memcached resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/crd/wordpresses.example.com.testproject.org.yaml
================================================
{{- if .Values.crd.enable }}
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    {{- if .Values.crd.keep }}
    "helm.sh/resource-policy": keep
    {{- end }}
    cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "project-v4-with-plugins.resourceName" (dict "suffix" "serving-cert" "context" $) }}
    controller-gen.kubebuilder.io/version: v0.20.1
  name: wordpresses.example.com.testproject.org
spec:
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        service:
          name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "webhook-service" "context" $) }}
          namespace: {{ .Release.Namespace }}
          path: /convert
      conversionReviewVersions:
      - v1
  group: example.com.testproject.org
  names:
    kind: Wordpress
    listKind: WordpressList
    plural: wordpresses
    singular: wordpress
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
  - name: v2
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: false
    subresources:
      status: {}
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/manager/manager.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    control-plane: controller-manager
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager" "context" $) }}
  namespace: {{ .Release.Namespace }}
spec:
  replicas: {{ .Values.manager.replicas }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
      control-plane: controller-manager
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
        helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
        app.kubernetes.io/instance: {{ .Release.Name }}
        app.kubernetes.io/managed-by: {{ .Release.Service }}
        control-plane: controller-manager
    spec:
      {{- with .Values.manager.tolerations }}
      tolerations: {{ toYaml . | nindent 10 }}
      {{- end }}
      {{- with .Values.manager.affinity }}
      affinity: {{ toYaml . | nindent 10 }}
      {{- end }}
      {{- with .Values.manager.nodeSelector }}
      nodeSelector: {{ toYaml . | nindent 10 }}
      {{- end }}
      containers:
      - args:
        {{- if .Values.metrics.enable }}
        - --metrics-bind-address=:{{ .Values.metrics.port }}
        {{- else }}
        # Bind to :0 to disable the controller-runtime managed metrics server
        - --metrics-bind-address=0
        {{- end }}
        - --health-probe-bind-address=:8081
        {{- range .Values.manager.args }}
        - {{ . }}
        {{- end }}
        {{- if .Values.certManager.enable }}
        - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs
        {{- end }}
        command:
        - /manager
        env:
{{- if or .Values.manager.env (and (kindIs "map" .Values.manager.envOverrides) (not (empty .Values.manager.envOverrides))) }}
          {{- if .Values.manager.env }}
          {{- toYaml .Values.manager.env | nindent 10 }}
          {{- end }}
          {{- if kindIs "map" .Values.manager.envOverrides }}
          {{- range $k, $v := .Values.manager.envOverrides }}
          - name: {{ $k }}
            value: {{ $v | quote }}
          {{ end }}
          {{- end }}
          {{- else }}
          []
          {{- end }}
        image: "{{ .Values.manager.image.repository }}:{{ .Values.manager.image.tag }}"
        imagePullPolicy: {{ .Values.manager.image.pullPolicy }}
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 15
          periodSeconds: 20
        name: manager
        ports:
        - containerPort: {{ .Values.webhook.port }}
          name: webhook-server
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          {{- if .Values.manager.resources }}
          {{- toYaml .Values.manager.resources | nindent 10 }}
          {{- else }}
          {}
          {{- end }}
        securityContext:
          {{- if .Values.manager.securityContext }}
          {{- toYaml .Values.manager.securityContext | nindent 10 }}
          {{- else }}
          {}
          {{- end }}
        volumeMounts:
        {{- if .Values.certManager.enable }}
        - mountPath: /tmp/k8s-webhook-server/serving-certs
          name: webhook-certs
          readOnly: true
        {{- end }}
      securityContext:
        {{- if .Values.manager.podSecurityContext }}
        {{- toYaml .Values.manager.podSecurityContext | nindent 8 }}
        {{- else }}
        {}
        {{- end }}
      serviceAccountName: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager" "context" $) }}
      terminationGracePeriodSeconds: 10
      volumes:
      {{- if .Values.certManager.enable }}
      - name: webhook-certs
        secret:
          secretName: webhook-server-cert
      {{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/metrics/controller-manager-metrics-service.yaml
================================================
{{- if .Values.metrics.enable }}
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    control-plane: controller-manager
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}
  namespace: {{ .Release.Namespace }}
spec:
  ports:
  - name: https
    port: {{ .Values.metrics.port }}
    protocol: TCP
    targetPort: {{ .Values.metrics.port }}
  selector:
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    control-plane: controller-manager
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/monitoring/servicemonitor.yaml
================================================
{{- if .Values.prometheus.enable }}
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    control-plane: controller-manager
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager-metrics-monitor" "context" $) }}
  namespace: {{ .Release.Namespace }}
spec:
  endpoints:
    - path: /metrics
      port: https
      scheme: https
      bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token
      tlsConfig:
        {{- if .Values.certManager.enable }}
        serverName: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager-metrics-service" "context" $) }}.{{ .Release.Namespace }}.svc
        # Apply secure TLS configuration with cert-manager
        insecureSkipVerify: false
        ca:
          secret:
            name: metrics-server-cert
            key: ca.crt
        cert:
          secret:
            name: metrics-server-cert
            key: tls.crt
        keySecret:
          name: metrics-server-cert
          key: tls.key
        {{- else }}
        # Development/Test mode (insecure configuration)
        insecureSkipVerify: true
        {{- end }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
      control-plane: controller-manager
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/busybox-admin-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "busybox-admin-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/busybox-editor-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "busybox-editor-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/busybox-viewer-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "busybox-viewer-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/controller-manager.yaml
================================================
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager" "context" $) }}
  namespace: {{ .Release.Namespace }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/leader-election-role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "leader-election-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/leader-election-rolebinding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "leader-election-rolebinding" "context" $) }}
  namespace: {{ .Release.Namespace }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "leader-election-role" "context" $) }}
subjects:
- kind: ServiceAccount
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager" "context" $) }}
  namespace: {{ .Release.Namespace }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/manager-role.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "manager-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - events.k8s.io
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  - memcacheds
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/finalizers
  - memcacheds/finalizers
  - wordpresses/finalizers
  verbs:
  - update
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  - memcacheds/status
  - wordpresses/status
  verbs:
  - get
  - patch
  - update


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/manager-rolebinding.yaml
================================================
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "manager-rolebinding" "context" $) }}
  namespace: {{ .Release.Namespace }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "manager-role" "context" $) }}
subjects:
- kind: ServiceAccount
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager" "context" $) }}
  namespace: {{ .Release.Namespace }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/memcached-admin-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "memcached-admin-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/memcached-editor-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "memcached-editor-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/memcached-viewer-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "memcached-viewer-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/metrics-auth-role.yaml
================================================
{{- if .Values.metrics.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }}
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/metrics-auth-rolebinding.yaml
================================================
{{- if .Values.metrics.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "metrics-auth-rolebinding" "context" $) }}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "metrics-auth-role" "context" $) }}
subjects:
- kind: ServiceAccount
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "controller-manager" "context" $) }}
  namespace: {{ .Release.Namespace }}
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/metrics-reader.yaml
================================================
{{- if .Values.metrics.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "metrics-reader" "context" $) }}
rules:
- nonResourceURLs:
  - /metrics
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/wordpress-admin-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "wordpress-admin-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/wordpress-editor-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "wordpress-editor-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/rbac/wordpress-viewer-role.yaml
================================================
{{- if .Values.rbacHelpers.enable }}
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "wordpress-viewer-role" "context" $) }}
  namespace: {{ .Release.Namespace }}
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/webhook/validating-webhook-configuration.yaml
================================================
{{- if .Values.webhook.enable }}
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    {{- if .Values.certManager.enable }}
    cert-manager.io/inject-ca-from: {{ .Release.Namespace }}/{{ include "project-v4-with-plugins.resourceName" (dict "suffix" "serving-cert" "context" $) }}
    {{- end }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "validating-webhook-configuration" "context" $) }}
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "webhook-service" "context" $) }}
      namespace: {{ .Release.Namespace }}
      path: /validate-example-com-testproject-org-v1alpha1-memcached
  failurePolicy: Fail
  name: vmemcached-v1alpha1.kb.io
  rules:
  - apiGroups:
    - example.com.testproject.org
    apiVersions:
    - v1alpha1
    operations:
    - CREATE
    - UPDATE
    resources:
    - memcacheds
  sideEffects: None
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/templates/webhook/webhook-service.yaml
================================================
{{- if .Values.webhook.enable }}
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
    app.kubernetes.io/instance: {{ .Release.Name }}
  name: {{ include "project-v4-with-plugins.resourceName" (dict "suffix" "webhook-service" "context" $) }}
  namespace: {{ .Release.Namespace }}
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: {{ .Values.webhook.port }}
  selector:
    app.kubernetes.io/name: {{ include "project-v4-with-plugins.name" . }}
    control-plane: controller-manager
{{- end }}


================================================
FILE: testdata/project-v4-with-plugins/dist/chart/values.yaml
================================================
## String to partially override chart.fullname template (will maintain the release name)
##
# nameOverride: ""

## String to fully override chart.fullname template
##
# fullnameOverride: ""

## Configure the controller manager deployment
##
manager:
  replicas: 1

  image:
    repository: controller
    tag: latest
    pullPolicy: IfNotPresent

  ## Arguments
  ##
  args:
    - --leader-elect

  ## Environment variables
  ##
  env:
    - name: BUSYBOX_IMAGE
      value: busybox:1.36.1
    - name: MEMCACHED_IMAGE
      value: memcached:1.6.26-alpine3.19
    - name: WATCH_NAMESPACE
      valueFrom:
        fieldRef:
          fieldPath: metadata.namespace

  ## Env overrides (--set manager.envOverrides.VAR=value)
  ## Same name in env above: this value takes precedence.
  ##
  envOverrides: {}

  ## Image pull secrets
  ##
  imagePullSecrets: []

  ## Pod-level security settings
  ##
  podSecurityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault

  ## Container-level security settings
  ##
  securityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop:
      - ALL
    readOnlyRootFilesystem: true

  ## Resource limits and requests
  ##
  resources:
    limits:
      cpu: 500m
      memory: 128Mi
    requests:
      cpu: 10m
      memory: 64Mi

  ## Manager pod's affinity
  ##
  affinity: {}

  ## Manager pod's node selector
  ##
  nodeSelector: {}

  ## Manager pod's tolerations
  ##
  tolerations: []

## Helper RBAC roles for managing custom resources
##
rbacHelpers:
  # Install convenience admin/editor/viewer roles for CRDs
  enable: false

## Custom Resource Definitions
##
crd:
  # Install CRDs with the chart
  enable: true
  # Keep CRDs when uninstalling
  keep: true

## Controller metrics endpoint.
## Enable to expose /metrics endpoint with RBAC protection.
##
metrics:
  enable: true
  # Metrics server port
  port: 8443

## Cert-manager integration for TLS certificates.
## Required for webhook certificates and metrics endpoint certificates.
##
certManager:
  enable: true

## Webhook server configuration
##
webhook:
  enable: true
  # Webhook server port
  port: 9443

## Prometheus ServiceMonitor for metrics scraping.
## Requires prometheus-operator to be installed in the cluster.
##
prometheus:
  enable: false



================================================
FILE: testdata/project-v4-with-plugins/dist/install.yaml
================================================
apiVersion: v1
kind: Namespace
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
    control-plane: controller-manager
  name: project-v4-with-plugins-system
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: busyboxes.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Busybox
    listKind: BusyboxList
    plural: busyboxes
    singular: busybox
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Busybox is the Schema for the busyboxes API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Busybox
            properties:
              size:
                default: 1
                description: size defines the number of Busybox instances
                format: int32
                minimum: 0
                type: integer
            type: object
          status:
            description: status defines the observed state of Busybox
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Busybox resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.20.1
  name: memcacheds.example.com.testproject.org
spec:
  group: example.com.testproject.org
  names:
    kind: Memcached
    listKind: MemcachedList
    plural: memcacheds
    singular: memcached
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        description: Memcached is the Schema for the memcacheds API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Memcached
            properties:
              containerPort:
                description: containerPort defines the port that will be used to init
                  the container with the image
                format: int32
                type: integer
              size:
                default: 1
                description: size defines the number of Memcached instances
                format: int32
                minimum: 0
                type: integer
            required:
            - containerPort
            type: object
          status:
            description: status defines the observed state of Memcached
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Memcached resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-with-plugins-system/project-v4-with-plugins-serving-cert
    controller-gen.kubebuilder.io/version: v0.20.1
  name: wordpresses.example.com.testproject.org
spec:
  conversion:
    strategy: Webhook
    webhook:
      clientConfig:
        service:
          name: project-v4-with-plugins-webhook-service
          namespace: project-v4-with-plugins-system
          path: /convert
      conversionReviewVersions:
      - v1
  group: example.com.testproject.org
  names:
    kind: Wordpress
    listKind: WordpressList
    plural: wordpresses
    singular: wordpress
  scope: Namespaced
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: true
    subresources:
      status: {}
  - name: v2
    schema:
      openAPIV3Schema:
        description: Wordpress is the Schema for the wordpresses API
        properties:
          apiVersion:
            description: |-
              APIVersion defines the versioned schema of this representation of an object.
              Servers should convert recognized schemas to the latest internal value, and
              may reject unrecognized values.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
            type: string
          kind:
            description: |-
              Kind is a string value representing the REST resource this object represents.
              Servers may infer this from the endpoint the client submits requests to.
              Cannot be updated.
              In CamelCase.
              More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
            type: string
          metadata:
            type: object
          spec:
            description: spec defines the desired state of Wordpress
            properties:
              foo:
                description: foo is an example field of Wordpress. Edit wordpress_types.go
                  to remove/update
                type: string
            type: object
          status:
            description: status defines the observed state of Wordpress
            properties:
              conditions:
                description: |-
                  conditions represent the current state of the Wordpress resource.
                  Each condition has a unique type and reflects the status of a specific aspect of the resource.

                  Standard condition types include:
                  - "Available": the resource is fully functional
                  - "Progressing": the resource is being created or updated
                  - "Degraded": the resource failed to reach or maintain its desired state

                  The status of each condition is one of True, False, or Unknown.
                items:
                  description: Condition contains details for one aspect of the current
                    state of this API Resource.
                  properties:
                    lastTransitionTime:
                      description: |-
                        lastTransitionTime is the last time the condition transitioned from one status to another.
                        This should be when the underlying condition changed.  If that is not known, then using the time when the API field changed is acceptable.
                      format: date-time
                      type: string
                    message:
                      description: |-
                        message is a human readable message indicating details about the transition.
                        This may be an empty string.
                      maxLength: 32768
                      type: string
                    observedGeneration:
                      description: |-
                        observedGeneration represents the .metadata.generation that the condition was set based upon.
                        For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
                        with respect to the current state of the instance.
                      format: int64
                      minimum: 0
                      type: integer
                    reason:
                      description: |-
                        reason contains a programmatic identifier indicating the reason for the condition's last transition.
                        Producers of specific condition types may define expected values and meanings for this field,
                        and whether the values are considered a guaranteed API.
                        The value should be a CamelCase string.
                        This field may not be empty.
                      maxLength: 1024
                      minLength: 1
                      pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
                      type: string
                    status:
                      description: status of the condition, one of True, False, Unknown.
                      enum:
                      - "True"
                      - "False"
                      - Unknown
                      type: string
                    type:
                      description: type of condition in CamelCase or in foo.example.com/CamelCase.
                      maxLength: 316
                      pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
                      type: string
                  required:
                  - lastTransitionTime
                  - message
                  - reason
                  - status
                  - type
                  type: object
                type: array
                x-kubernetes-list-map-keys:
                - type
                x-kubernetes-list-type: map
            type: object
        required:
        - spec
        type: object
    served: true
    storage: false
    subresources:
      status: {}
---
apiVersion: v1
kind: ServiceAccount
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-controller-manager
  namespace: project-v4-with-plugins-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-busybox-admin-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-busybox-editor-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-busybox-viewer-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-leader-election-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - coordination.k8s.io
  resources:
  - leases
  verbs:
  - get
  - list
  - watch
  - create
  - update
  - patch
  - delete
- apiGroups:
  - ""
  resources:
  - events
  verbs:
  - create
  - patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: project-v4-with-plugins-manager-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - apps
  resources:
  - deployments
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - events.k8s.io
  resources:
  - events
  verbs:
  - create
  - patch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes
  - memcacheds
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/finalizers
  - memcacheds/finalizers
  - wordpresses/finalizers
  verbs:
  - update
- apiGroups:
  - example.com.testproject.org
  resources:
  - busyboxes/status
  - memcacheds/status
  - wordpresses/status
  verbs:
  - get
  - patch
  - update
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-memcached-admin-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-memcached-editor-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-memcached-viewer-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - memcacheds/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-wordpress-admin-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - '*'
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-wordpress-editor-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - create
  - delete
  - get
  - list
  - patch
  - update
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-wordpress-viewer-role
  namespace: project-v4-with-plugins-system
rules:
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses
  verbs:
  - get
  - list
  - watch
- apiGroups:
  - example.com.testproject.org
  resources:
  - wordpresses/status
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-with-plugins-metrics-auth-role
rules:
- apiGroups:
  - authentication.k8s.io
  resources:
  - tokenreviews
  verbs:
  - create
- apiGroups:
  - authorization.k8s.io
  resources:
  - subjectaccessreviews
  verbs:
  - create
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: project-v4-with-plugins-metrics-reader
rules:
- nonResourceURLs:
  - /metrics
  verbs:
  - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-leader-election-rolebinding
  namespace: project-v4-with-plugins-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: project-v4-with-plugins-leader-election-role
subjects:
- kind: ServiceAccount
  name: project-v4-with-plugins-controller-manager
  namespace: project-v4-with-plugins-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-manager-rolebinding
  namespace: project-v4-with-plugins-system
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: project-v4-with-plugins-manager-role
subjects:
- kind: ServiceAccount
  name: project-v4-with-plugins-controller-manager
  namespace: project-v4-with-plugins-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: project-v4-with-plugins-metrics-auth-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: project-v4-with-plugins-metrics-auth-role
subjects:
- kind: ServiceAccount
  name: project-v4-with-plugins-controller-manager
  namespace: project-v4-with-plugins-system
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
    control-plane: controller-manager
  name: project-v4-with-plugins-controller-manager-metrics-service
  namespace: project-v4-with-plugins-system
spec:
  ports:
  - name: https
    port: 8443
    protocol: TCP
    targetPort: 8443
  selector:
    app.kubernetes.io/name: project-v4-with-plugins
    control-plane: controller-manager
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-webhook-service
  namespace: project-v4-with-plugins-system
spec:
  ports:
  - port: 443
    protocol: TCP
    targetPort: 9443
  selector:
    app.kubernetes.io/name: project-v4-with-plugins
    control-plane: controller-manager
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
    control-plane: controller-manager
  name: project-v4-with-plugins-controller-manager
  namespace: project-v4-with-plugins-system
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: project-v4-with-plugins
      control-plane: controller-manager
  template:
    metadata:
      annotations:
        kubectl.kubernetes.io/default-container: manager
      labels:
        app.kubernetes.io/name: project-v4-with-plugins
        control-plane: controller-manager
    spec:
      containers:
      - args:
        - --metrics-bind-address=:8443
        - --leader-elect
        - --health-probe-bind-address=:8081
        - --webhook-cert-path=/tmp/k8s-webhook-server/serving-certs
        command:
        - /manager
        env:
        - name: BUSYBOX_IMAGE
          value: busybox:1.36.1
        - name: MEMCACHED_IMAGE
          value: memcached:1.6.26-alpine3.19
        - name: WATCH_NAMESPACE
          valueFrom:
            fieldRef:
              fieldPath: metadata.namespace
        image: controller:latest
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 15
          periodSeconds: 20
        name: manager
        ports:
        - containerPort: 9443
          name: webhook-server
          protocol: TCP
        readinessProbe:
          httpGet:
            path: /readyz
            port: 8081
          initialDelaySeconds: 5
          periodSeconds: 10
        resources:
          limits:
            cpu: 500m
            memory: 128Mi
          requests:
            cpu: 10m
            memory: 64Mi
        securityContext:
          allowPrivilegeEscalation: false
          capabilities:
            drop:
            - ALL
          readOnlyRootFilesystem: true
        volumeMounts:
        - mountPath: /tmp/k8s-webhook-server/serving-certs
          name: webhook-certs
          readOnly: true
      securityContext:
        runAsNonRoot: true
        seccompProfile:
          type: RuntimeDefault
      serviceAccountName: project-v4-with-plugins-controller-manager
      terminationGracePeriodSeconds: 10
      volumes:
      - name: webhook-certs
        secret:
          secretName: webhook-server-cert
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-metrics-certs
  namespace: project-v4-with-plugins-system
spec:
  dnsNames:
  - SERVICE_NAME.SERVICE_NAMESPACE.svc
  - SERVICE_NAME.SERVICE_NAMESPACE.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: project-v4-with-plugins-selfsigned-issuer
  secretName: metrics-server-cert
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-serving-cert
  namespace: project-v4-with-plugins-system
spec:
  dnsNames:
  - project-v4-with-plugins-webhook-service.project-v4-with-plugins-system.svc
  - project-v4-with-plugins-webhook-service.project-v4-with-plugins-system.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: project-v4-with-plugins-selfsigned-issuer
  secretName: webhook-server-cert
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  labels:
    app.kubernetes.io/managed-by: kustomize
    app.kubernetes.io/name: project-v4-with-plugins
  name: project-v4-with-plugins-selfsigned-issuer
  namespace: project-v4-with-plugins-system
spec:
  selfSigned: {}
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  annotations:
    cert-manager.io/inject-ca-from: project-v4-with-plugins-system/project-v4-with-plugins-serving-cert
  name: project-v4-with-plugins-validating-webhook-configuration
webhooks:
- admissionReviewVersions:
  - v1
  clientConfig:
    service:
      name: project-v4-with-plugins-webhook-service
      namespace: project-v4-with-plugins-system
      path: /validate-example-com-testproject-org-v1alpha1-memcached
  failurePolicy: Fail
  name: vmemcached-v1alpha1.kb.io
  rules:
  - apiGroups:
    - example.com.testproject.org
    apiVersions:
    - v1alpha1
    operations:
    - CREATE
    - UPDATE
    resources:
    - memcacheds
  sideEffects: None


================================================
FILE: testdata/project-v4-with-plugins/go.mod
================================================
module sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins

go 1.25.3

require (
	github.com/onsi/ginkgo/v2 v2.27.2
	github.com/onsi/gomega v1.38.2
	k8s.io/api v0.35.0
	k8s.io/apimachinery v0.35.0
	k8s.io/client-go v0.35.0
	k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
	sigs.k8s.io/controller-runtime v0.23.3
)

require (
	cel.dev/expr v0.24.0 // indirect
	github.com/Masterminds/semver/v3 v3.4.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.1 // indirect
	github.com/emicklei/go-restful/v3 v3.12.2 // indirect
	github.com/evanphx/json-patch/v5 v5.9.11 // indirect
	github.com/felixge/httpsnoop v1.0.4 // indirect
	github.com/fsnotify/fsnotify v1.9.0 // 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-logr/zapr v1.3.0 // indirect
	github.com/go-openapi/jsonpointer v0.21.0 // indirect
	github.com/go-openapi/jsonreference v0.20.2 // indirect
	github.com/go-openapi/swag v0.23.0 // indirect
	github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
	github.com/google/btree v1.1.3 // 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/pprof v0.0.0-20250403155104-27863c87afa6 // 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.0 // 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.uber.org/multierr v1.11.0 // indirect
	go.uber.org/zap v1.27.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.29.0 // indirect
	golang.org/x/net v0.47.0 // indirect
	golang.org/x/oauth2 v0.30.0 // indirect
	golang.org/x/sync v0.18.0 // indirect
	golang.org/x/sys v0.38.0 // indirect
	golang.org/x/term v0.37.0 // indirect
	golang.org/x/text v0.31.0 // indirect
	golang.org/x/time v0.9.0 // indirect
	golang.org/x/tools v0.38.0 // indirect
	gomodules.xyz/jsonpatch/v2 v2.4.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.v3 v3.0.1 // indirect
	k8s.io/apiextensions-apiserver v0.35.0 // 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/kube-openapi v0.0.0-20250910181357-589584f1c912 // 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.2-0.20260122202528-d9cc6641c482 // indirect
	sigs.k8s.io/yaml v1.6.0 // indirect
)


================================================
FILE: testdata/project-v4-with-plugins/grafana/controller-resources-metrics.json
================================================
{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "1.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": "-- Grafana --",
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "percent"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 0,
        "y": 0
      },
      "id": 2,
      "interval": "1m",
      "links": [],
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "rate(process_cpu_seconds_total{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}[5m]) * 100",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "Pod: {{pod}} | Container: {{container}}",
          "refId": "A",
          "step": 10
        }
      ],
      "title": "Controller CPU Usage",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "bytes"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 12,
        "x": 12,
        "y": 0
      },
      "id": 4,
      "interval": "1m",
      "links": [],
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "list",
          "placement": "bottom"
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "process_resident_memory_bytes{job=\"$job\", namespace=\"$namespace\", pod=\"$pod\"}",
          "format": "time_series",
          "interval": "",
          "intervalFactor": 2,
          "legendFormat": "Pod: {{pod}} | Container: {{container}}",
          "refId": "A",
          "step": 10
        }
      ],
      "title": "Controller Memory Usage",
      "type": "timeseries"
    }
  ],
  "refresh": "",
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "job",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "observability",
          "value": "observability"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total, namespace)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "namespace",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total, namespace)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": false,
          "text": "All",
          "value": "$__all"
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
        "hide": 2,
        "includeAll": true,
        "label": "pod",
        "multi": true,
        "name": "pod",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Controller-Resources-Metrics",
  "weekStart": ""
}


================================================
FILE: testdata/project-v4-with-plugins/grafana/controller-runtime-metrics.json
================================================
{
  "__inputs": [
    {
      "name": "DS_PROMETHEUS",
      "label": "Prometheus",
      "description": "",
      "type": "datasource",
      "pluginId": "prometheus",
      "pluginName": "Prometheus"
    }
  ],
  "__requires": [
    {
      "type": "datasource",
      "id": "prometheus",
      "name": "Prometheus",
      "version": "1.0.0"
    }
  ],
  "annotations": {
    "list": [
      {
        "builtIn": 1,
        "datasource": {
          "type": "datasource",
          "uid": "grafana"
        },
        "enable": true,
        "hide": true,
        "iconColor": "rgba(0, 211, 255, 1)",
        "name": "Annotations & Alerts",
        "target": {
          "limit": 100,
          "matchAny": false,
          "tags": [],
          "type": "dashboard"
        },
        "type": "dashboard"
      }
    ]
  },
  "editable": true,
  "fiscalYearStartMonth": 0,
  "graphTooltip": 0,
  "links": [],
  "liveNow": false,
  "panels": [
    {
      "collapsed": false,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 0
      },
      "id": 9,
      "panels": [],
      "title": "Reconciliation Metrics",
      "type": "row"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 3,
        "x": 0,
        "y": 1
      },
      "id": 24,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "controller_runtime_active_workers{job=\"$job\", namespace=\"$namespace\"}",
          "interval": "",
          "legendFormat": "{{controller}} {{instance}}",
          "refId": "A"
        }
      ],
      "title": "Number of workers in use",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of reconciliations per controller",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "cpm"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 11,
        "x": 3,
        "y": 1
      },
      "id": 7,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "editorMode": "code",
          "exemplar": true,
          "expr": "sum(rate(controller_runtime_reconcile_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
          "interval": "",
          "legendFormat": "{{instance}} {{pod}}",
          "range": true,
          "refId": "A"
        }
      ],
      "title": "Total Reconciliation Count Per Controller",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of reconciliation errors per controller",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "cpm"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 10,
        "x": 14,
        "y": 1
      },
      "id": 6,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "editorMode": "code",
          "exemplar": true,
          "expr": "sum(rate(controller_runtime_reconcile_errors_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)",
          "interval": "",
          "legendFormat": "{{instance}} {{pod}}",
          "range": true,
          "refId": "A"
        }
      ],
      "title": "Reconciliation Error Count Per Controller",
      "type": "timeseries"
    },
    {
      "collapsed": false,
      "gridPos": {
        "h": 1,
        "w": 24,
        "x": 0,
        "y": 9
      },
      "id": 11,
      "panels": [],
      "title": "Work Queue Metrics",
      "type": "row"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          }
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 3,
        "x": 0,
        "y": 10
      },
      "id": 22,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "workqueue_depth{job=\"$job\", namespace=\"$namespace\"}",
          "interval": "",
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "WorkQueue Depth",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How long in seconds an item stays in workqueue before being requested",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 10,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "normal"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 11,
        "x": 3,
        "y": 10
      },
      "id": 13,
      "options": {
        "legend": {
          "calcs": [
            "max",
            "mean"
          ],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.50, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "interval": "",
          "legendFormat": "P50 {{name}} {{instance}} ",
          "refId": "A"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.90, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P90 {{name}} {{instance}} ",
          "refId": "B"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.99, sum(rate(workqueue_queue_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P99 {{name}} {{instance}} ",
          "refId": "C"
        }
      ],
      "title": "Seconds For Items Stay In Queue (before being requested) (P50, P90, P99)",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "ops"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 8,
        "w": 10,
        "x": 14,
        "y": 10
      },
      "id": 15,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "pluginVersion": "8.4.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "sum(rate(workqueue_adds_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
          "interval": "",
          "legendFormat": "{{name}} {{instance}}",
          "refId": "A"
        }
      ],
      "title": "Work Queue Add Rate",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How many seconds of work has done that is in progress and hasn't been observed by work_duration.\nLarge values indicate stuck threads.\nOne can deduce the number of stuck threads by observing the rate at which this increases.",
      "fieldConfig": {
        "defaults": {
          "mappings": [],
          "thresholds": {
            "mode": "percentage",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "orange",
                "value": 70
              },
              {
                "color": "red",
                "value": 85
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 3,
        "x": 0,
        "y": 18
      },
      "id": 23,
      "options": {
        "orientation": "auto",
        "reduceOptions": {
          "calcs": ["lastNotNull"],
          "fields": "",
          "values": false
        },
        "showThresholdLabels": false,
        "showThresholdMarkers": true
      },
      "pluginVersion": "9.5.3",
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "rate(workqueue_unfinished_work_seconds{job=\"$job\", namespace=\"$namespace\"}[5m])",
          "interval": "",
          "legendFormat": "",
          "refId": "A"
        }
      ],
      "title": "Unfinished Seconds",
      "type": "gauge"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "How long in seconds processing an item from workqueue takes.",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "palette-classic"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 10,
            "gradientMode": "none",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "linear",
            "lineWidth": 1,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "s"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 11,
        "x": 3,
        "y": 18
      },
      "id": 19,
      "options": {
        "legend": {
          "calcs": [
            "max",
            "mean"
          ],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.50, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "interval": "",
          "legendFormat": "P50 {{name}} {{instance}} ",
          "refId": "A"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.90, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P90 {{name}} {{instance}} ",
          "refId": "B"
        },
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "histogram_quantile(0.99, sum(rate(workqueue_work_duration_seconds_bucket{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name, le))",
          "hide": false,
          "interval": "",
          "legendFormat": "P99 {{name}} {{instance}} ",
          "refId": "C"
        }
      ],
      "title": "Seconds Processing Items From WorkQueue (P50, P90, P99)",
      "type": "timeseries"
    },
    {
      "datasource": "${DS_PROMETHEUS}",
      "description": "Total number of retries handled by workqueue",
      "fieldConfig": {
        "defaults": {
          "color": {
            "mode": "continuous-GrYlRd"
          },
          "custom": {
            "axisCenteredZero": false,
            "axisColorMode": "text",
            "axisLabel": "",
            "axisPlacement": "auto",
            "barAlignment": 0,
            "drawStyle": "line",
            "fillOpacity": 20,
            "gradientMode": "scheme",
            "hideFrom": {
              "legend": false,
              "tooltip": false,
              "viz": false
            },
            "lineInterpolation": "smooth",
            "lineWidth": 3,
            "pointSize": 5,
            "scaleDistribution": {
              "type": "linear"
            },
            "showPoints": "auto",
            "spanNulls": false,
            "stacking": {
              "group": "A",
              "mode": "none"
            },
            "thresholdsStyle": {
              "mode": "off"
            }
          },
          "mappings": [],
          "thresholds": {
            "mode": "absolute",
            "steps": [
              {
                "color": "green",
                "value": null
              },
              {
                "color": "red",
                "value": 80
              }
            ]
          },
          "unit": "ops"
        },
        "overrides": []
      },
      "gridPos": {
        "h": 9,
        "w": 10,
        "x": 14,
        "y": 18
      },
      "id": 17,
      "options": {
        "legend": {
          "calcs": [],
          "displayMode": "table",
          "placement": "bottom",
          "showLegend": true
        },
        "tooltip": {
          "mode": "single",
          "sort": "none"
        }
      },
      "targets": [
        {
          "datasource": "${DS_PROMETHEUS}",
          "exemplar": true,
          "expr": "sum(rate(workqueue_retries_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, name)",
          "interval": "",
          "legendFormat": "{{name}} {{instance}} ",
          "refId": "A"
        }
      ],
      "title": "Work Queue Retries Rate",
      "type": "timeseries"
    }
  ],
  "refresh": "",
  "style": "dark",
  "tags": [],
  "templating": {
    "list": [
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "job",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total, namespace)",
        "hide": 0,
        "includeAll": false,
        "multi": false,
        "name": "namespace",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total, namespace)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 1,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      },
      {
        "current": {
          "selected": true,
          "text": [
            "All"
          ],
          "value": [
            "$__all"
          ]
        },
        "datasource": "${DS_PROMETHEUS}",
        "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
        "hide": 2,
        "includeAll": true,
        "label": "pod",
        "multi": true,
        "name": "pod",
        "options": [],
        "query": {
          "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)",
          "refId": "StandardVariableQuery"
        },
        "refresh": 2,
        "regex": "",
        "skipUrlSync": false,
        "sort": 0,
        "type": "query"
      }
    ]
  },
  "time": {
    "from": "now-15m",
    "to": "now"
  },
  "timepicker": {},
  "timezone": "",
  "title": "Controller-Runtime-Metrics",
  "weekStart": ""
}


================================================
FILE: testdata/project-v4-with-plugins/grafana/custom-metrics/config.yaml
================================================
---
customMetrics:
#  - metric: # Raw custom metric (required)
#    type:   # Metric type: counter/gauge/histogram (required)
#    expr:   # Prom_ql for the metric (optional)
#    unit:   # Unit of measurement, examples: s,none,bytes,percent,etc. (optional)
#
#
# Example:
# ---
# customMetrics:
#   - metric: foo_bar
#     unit: none
#     type: histogram
#   	expr: histogram_quantile(0.90, sum by(instance, le) (rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m])))


================================================
FILE: testdata/project-v4-with-plugins/hack/boilerplate.go.txt
================================================
/*
Copyright 2026 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: testdata/project-v4-with-plugins/internal/controller/busybox_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	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/types"
	"k8s.io/client-go/tools/events"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
)

const busyboxFinalizer = "example.com.testproject.org/finalizer"

// Definitions to manage status conditions
const (
	// typeAvailableBusybox represents the status of the Deployment reconciliation
	typeAvailableBusybox = "Available"
	// typeDegradedBusybox represents the status used when the custom resource is deleted and the finalizer operations are yet to occur.
	typeDegradedBusybox = "Degraded"
)

// BusyboxReconciler reconciles a Busybox object
type BusyboxReconciler struct {
	client.Client
	Scheme   *runtime.Scheme
	Recorder events.EventRecorder
}

// The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen
// when the command  is executed.
// To know more about markers see: https://book.kubebuilder.io/reference/markers.html

// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=busyboxes,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=busyboxes/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=busyboxes/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,namespace=project-v4-with-plugins-system,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,namespace=project-v4-with-plugins-system,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,namespace=project-v4-with-plugins-system,resources=pods,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator
// pattern you will create Controllers which provide a reconcile function
// responsible for synchronizing resources until the desired state is reached on the cluster.
// Breaking this recommendation goes against the design principles of controller-runtime.
// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention.
// For further info:
// - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
// - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := logf.FromContext(ctx)

	// Fetch the Busybox instance
	// The purpose is check if the Custom Resource for the Kind Busybox
	// is applied on the cluster if not we return nil to stop the reconciliation
	busybox := &examplecomv1alpha1.Busybox{}
	err := r.Get(ctx, req.NamespacedName, busybox)
	if err != nil {
		if apierrors.IsNotFound(err) {
			// If the custom resource is not found then it usually means that it was deleted or not created
			// In this way, we will stop the reconciliation
			log.Info("Busybox resource not found, ignoring since object must be deleted")
			return ctrl.Result{}, nil
		}
		// Error reading the object - requeue the request.
		log.Error(err, "Failed to get busybox")
		return ctrl.Result{}, err
	}

	if len(busybox.Status.Conditions) == 0 {
		meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"})
		if err = r.Status().Update(ctx, busybox); err != nil {
			log.Error(err, "Failed to update Busybox status")
			return ctrl.Result{}, err
		}

		// Let's re-fetch the busybox Custom Resource after updating the status
		// so that we have the latest state of the resource on the cluster and we will avoid
		// raising the error "the object has been modified, please apply
		// your changes to the latest version and try again" which would re-trigger the reconciliation
		// if we try to update it again in the following operations
		if err := r.Get(ctx, req.NamespacedName, busybox); err != nil {
			log.Error(err, "Failed to re-fetch busybox")
			return ctrl.Result{}, err
		}
	}

	// Let's add a finalizer. Then, we can define some operations which should
	// occur before the custom resource is deleted.
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers
	if !controllerutil.ContainsFinalizer(busybox, busyboxFinalizer) {
		log.Info("Adding finalizer for Busybox")
		controllerutil.AddFinalizer(busybox, busyboxFinalizer)
		if err = r.Update(ctx, busybox); err != nil {
			log.Error(err, "Failed to update custom resource to add finalizer")
			return ctrl.Result{}, err
		}
	}

	// Check if the Busybox instance is marked to be deleted, which is
	// indicated by the deletion timestamp being set.
	isBusyboxMarkedToBeDeleted := busybox.GetDeletionTimestamp() != nil
	if isBusyboxMarkedToBeDeleted {
		if controllerutil.ContainsFinalizer(busybox, busyboxFinalizer) {
			log.Info("Performing finalizer operations for Busybox before deleting CR")

			// Let's add here a status "Downgrade" to reflect that this resource began its process to be terminated.
			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeDegradedBusybox,
				Status: metav1.ConditionUnknown, Reason: "Finalizing",
				Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", busybox.Name)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			// Perform all operations required before removing the finalizer and allow
			// the Kubernetes API to remove the custom resource.
			r.doFinalizerOperationsForBusybox(busybox)

			// TODO(user): If you add operations to the doFinalizerOperationsForBusybox method
			// then you need to ensure that all worked fine before deleting and updating the Downgrade status
			// otherwise, you should requeue here.

			// Re-fetch the busybox Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, busybox); err != nil {
				log.Error(err, "Failed to re-fetch busybox")
				return ctrl.Result{}, err
			}

			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeDegradedBusybox,
				Status: metav1.ConditionTrue, Reason: "Finalizing",
				Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", busybox.Name)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			log.Info("Removing finalizer for Busybox after successfully performing the operations")
			if ok := controllerutil.RemoveFinalizer(busybox, busyboxFinalizer); !ok {
				err = fmt.Errorf("finalizer for Busybox was not removed")
				log.Error(err, "Failed to remove finalizer for Busybox")
				return ctrl.Result{}, err
			}

			if err := r.Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to remove finalizer for Busybox")
				return ctrl.Result{}, err
			}
		}
		return ctrl.Result{}, nil
	}

	// Check if the deployment already exists, if not create a new one
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: busybox.Name, Namespace: busybox.Namespace}, found)
	if err != nil && apierrors.IsNotFound(err) {
		// Define a new deployment
		dep, err := r.deploymentForBusybox(busybox)
		if err != nil {
			log.Error(err, "Failed to define new Deployment resource for Busybox")

			// The following implementation will update the status
			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox,
				Status: metav1.ConditionFalse, Reason: "Reconciling",
				Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", busybox.Name, err)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		log.Info("Creating a new Deployment",
			"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		if err = r.Create(ctx, dep); err != nil {
			log.Error(err, "Failed to create new Deployment",
				"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}

		// Deployment created successfully
		// We will requeue the reconciliation so that we can ensure the state
		// and move forward for the next operations
		return ctrl.Result{RequeueAfter: time.Minute}, nil
	} else if err != nil {
		log.Error(err, "Failed to get Deployment")
		// Let's return the error for the reconciliation be re-triggered again
		return ctrl.Result{}, err
	}

	// If the size is not defined in the Custom Resource then we will set the desired replicas to 0
	var desiredReplicas int32 = 0
	if busybox.Spec.Size != nil {
		desiredReplicas = *busybox.Spec.Size
	}

	// The CRD API defines that the Busybox type have a BusyboxSpec.Size field
	// to set the quantity of Deployment instances to the desired state on the cluster.
	// Therefore, the following code will ensure the Deployment size is the same as defined
	// via the Size spec of the Custom Resource which we are reconciling.
	if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas {
		found.Spec.Replicas = ptr.To(desiredReplicas)
		if err = r.Update(ctx, found); err != nil {
			log.Error(err, "Failed to update Deployment",
				"Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)

			// Re-fetch the busybox Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, busybox); err != nil {
				log.Error(err, "Failed to re-fetch busybox")
				return ctrl.Result{}, err
			}

			// The following implementation will update the status
			meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox,
				Status: metav1.ConditionFalse, Reason: "Resizing",
				Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", busybox.Name, err)})

			if err := r.Status().Update(ctx, busybox); err != nil {
				log.Error(err, "Failed to update Busybox status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		// Now, that we update the size we want to requeue the reconciliation
		// so that we can ensure that we have the latest state of the resource before
		// update. Also, it will help ensure the desired state on the cluster
		return ctrl.Result{Requeue: true}, nil
	}

	// The following implementation will update the status
	meta.SetStatusCondition(&busybox.Status.Conditions, metav1.Condition{Type: typeAvailableBusybox,
		Status: metav1.ConditionTrue, Reason: "Reconciling",
		Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", busybox.Name, desiredReplicas)})

	if err := r.Status().Update(ctx, busybox); err != nil {
		log.Error(err, "Failed to update Busybox status")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

// finalizeBusybox will perform the required operations before delete the CR.
func (r *BusyboxReconciler) doFinalizerOperationsForBusybox(cr *examplecomv1alpha1.Busybox) {
	// TODO(user): Add the cleanup steps that the operator
	// needs to do before the CR can be deleted. Examples
	// of finalizers include performing backups and deleting
	// resources that are not owned by this CR, like a PVC.

	// Note: It is not recommended to use finalizers with the purpose of deleting resources which are
	// created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile,
	// are defined as dependent of the custom resource. See that we use the method ctrl.SetControllerReference.
	// to set the ownerRef which means that the Deployment will be deleted by the Kubernetes API.
	// More info: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/

	// The following implementation will raise an event
	r.Recorder.Eventf(cr, nil, corev1.EventTypeWarning, "Deleting", "DeleteCR",
		"Custom Resource %s is being deleted from the namespace %s",
		cr.Name,
		cr.Namespace)
}

// deploymentForBusybox returns a Busybox Deployment object
func (r *BusyboxReconciler) deploymentForBusybox(
	busybox *examplecomv1alpha1.Busybox) (*appsv1.Deployment, error) {
	ls := labelsForBusybox()

	// Get the Operand image
	image, err := imageForBusybox()
	if err != nil {
		return nil, err
	}

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      busybox.Name,
			Namespace: busybox.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: busybox.Spec.Size,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: corev1.PodSpec{
					// TODO(user): Uncomment the following code to configure the nodeAffinity expression
					// according to the platforms which are supported by your solution. It is considered
					// best practice to support multiple architectures. build your manager image using the
					// makefile target docker-buildx. Also, you can use docker manifest inspect 
					// to check what are the platforms supported.
					// More info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
					// Affinity: &corev1.Affinity{
					//	 NodeAffinity: &corev1.NodeAffinity{
					//		 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
					//			 NodeSelectorTerms: []corev1.NodeSelectorTerm{
					//				 {
					//					 MatchExpressions: []corev1.NodeSelectorRequirement{
					//						 {
					//							 Key:      "kubernetes.io/arch",
					//							 Operator: "In",
					//							 Values:   []string{"amd64", "arm64", "ppc64le", "s390x"},
					//						 },
					//						 {
					//							 Key:      "kubernetes.io/os",
					//							 Operator: "In",
					//							 Values:   []string{"linux"},
					//						 },
					//					 },
					//				 },
					//		 	 },
					//		 },
					//	 },
					// },
					SecurityContext: &corev1.PodSecurityContext{
						RunAsNonRoot: ptr.To(true),
						// IMPORTANT: seccomProfile was introduced with Kubernetes 1.19
						// If you are looking for to produce solutions to be supported
						// on lower versions you must remove this option.
						SeccompProfile: &corev1.SeccompProfile{
							Type: corev1.SeccompProfileTypeRuntimeDefault,
						},
					},
					Containers: []corev1.Container{{
						Image:           image,
						Name:            "busybox",
						ImagePullPolicy: corev1.PullIfNotPresent,
						// Ensure restrictive context for the container
						// More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
						SecurityContext: &corev1.SecurityContext{
							RunAsNonRoot:             ptr.To(true),
							AllowPrivilegeEscalation: ptr.To(false),
							Capabilities: &corev1.Capabilities{
								Drop: []corev1.Capability{
									"ALL",
								},
							},
						},
					}},
				},
			},
		},
	}

	// Set the ownerRef for the Deployment
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/
	if err := ctrl.SetControllerReference(busybox, dep, r.Scheme); err != nil {
		return nil, err
	}
	return dep, nil
}

// labelsForBusybox returns the labels for selecting the resources
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
func labelsForBusybox() map[string]string {
	var imageTag string
	image, err := imageForBusybox()
	if err == nil {
		imageTag = strings.Split(image, ":")[1]
	}
	return map[string]string{
		"app.kubernetes.io/name":       "project-v4-with-plugins",
		"app.kubernetes.io/version":    imageTag,
		"app.kubernetes.io/managed-by": "BusyboxController",
	}
}

// imageForBusybox gets the Operand image which is managed by this controller
// from the BUSYBOX_IMAGE environment variable defined in the config/manager/manager.yaml
func imageForBusybox() (string, error) {
	var imageEnvVar = "BUSYBOX_IMAGE"
	image, found := os.LookupEnv(imageEnvVar)
	if !found {
		return "", fmt.Errorf("unable to find %s environment variable with the image", imageEnvVar)
	}
	return image, nil
}

// SetupWithManager sets up the controller with the Manager.
// The whole idea is to be watching the resources that matter for the controller.
// When a resource that the controller is interested in changes, the Watch triggers
// the controller’s reconciliation loop, ensuring that the actual state of the resource
// matches the desired state as defined in the controller’s logic.
//
// Notice how we configured the Manager to monitor events such as the creation, update,
// or deletion of a Custom Resource (CR) of the Busybox kind, as well as any changes
// to the Deployment that the controller manages and owns.
func (r *BusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		// Watch the Busybox CR(s) and trigger reconciliation whenever it
		// is created, updated, or deleted
		For(&examplecomv1alpha1.Busybox{}).
		Named("busybox").
		// Watch the Deployment managed by the BusyboxReconciler. If any changes occur to the Deployment
		// owned and managed by this controller, it will trigger reconciliation, ensuring that the cluster
		// state aligns with the desired state. See that the ownerRef was set when the Deployment was created.
		Owns(&appsv1.Deployment{}).
		Complete(r)
}


================================================
FILE: testdata/project-v4-with-plugins/internal/controller/busybox_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"
	"os"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
)

var _ = Describe("Busybox controller", func() {
	Context("Busybox controller test", func() {

		const BusyboxName = "test-busybox"

		ctx := context.Background()

		namespace := &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name:      BusyboxName,
				Namespace: BusyboxName,
			},
		}

		typeNamespacedName := types.NamespacedName{
			Name:      BusyboxName,
			Namespace: BusyboxName,
		}
		busybox := &examplecomv1alpha1.Busybox{}

		SetDefaultEventuallyTimeout(2 * time.Minute)
		SetDefaultEventuallyPollingInterval(time.Second)

		BeforeEach(func() {
			By("Creating the Namespace to perform the tests")
			err := k8sClient.Create(ctx, namespace)
			Expect(err).NotTo(HaveOccurred())

			By("Setting the Image ENV VAR which stores the Operand image")
			err = os.Setenv("BUSYBOX_IMAGE", "example.com/image:test")
			Expect(err).NotTo(HaveOccurred())

			By("creating the custom resource for the Kind Busybox")
			err = k8sClient.Get(ctx, typeNamespacedName, busybox)
			if err != nil && errors.IsNotFound(err) {
				// Let's mock our custom resource at the same way that we would
				// apply on the cluster the manifest under config/samples
				busybox = &examplecomv1alpha1.Busybox{
					ObjectMeta: metav1.ObjectMeta{
						Name:      BusyboxName,
						Namespace: namespace.Name,
					},
					Spec: examplecomv1alpha1.BusyboxSpec{
						Size: ptr.To(int32(1)),
					},
				}

				err = k8sClient.Create(ctx, busybox)
				Expect(err).NotTo(HaveOccurred())
			}
		})

		AfterEach(func() {
			By("removing the custom resource for the Kind Busybox")
			found := &examplecomv1alpha1.Busybox{}
			err := k8sClient.Get(ctx, typeNamespacedName, found)
			Expect(err).NotTo(HaveOccurred())

			Eventually(func(g Gomega) {
				g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed())
			}).Should(Succeed())

			// TODO(user): Attention if you improve this code by adding other context test you MUST
			// be aware of the current delete namespace limitations.
			// More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations
			By("Deleting the Namespace to perform the tests")
			_ = k8sClient.Delete(ctx, namespace)

			By("Removing the Image ENV VAR which stores the Operand image")
			_ = os.Unsetenv("BUSYBOX_IMAGE")
		})

		It("should successfully reconcile a custom resource for Busybox", func() {
			By("Checking if the custom resource was successfully created")
			Eventually(func(g Gomega) {
				found := &examplecomv1alpha1.Busybox{}
				Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource created")
			busyboxReconciler := &BusyboxReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := busyboxReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking if Deployment was successfully created in the reconciliation")
			Eventually(func(g Gomega) {
				found := &appsv1.Deployment{}
				g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource again")
			_, err = busyboxReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking the latest Status Condition added to the Busybox instance")
			Expect(k8sClient.Get(ctx, typeNamespacedName, busybox)).To(Succeed())
			var conditions []metav1.Condition
			Expect(busybox.Status.Conditions).To(ContainElement(
				HaveField("Type", Equal(typeAvailableBusybox)), &conditions))
			Expect(conditions).To(HaveLen(1), "Multiple conditions of type %s", typeAvailableBusybox)
			Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "condition %s", typeAvailableBusybox)
			Expect(conditions[0].Reason).To(Equal("Reconciling"), "condition %s", typeAvailableBusybox)
		})
	})
})


================================================
FILE: testdata/project-v4-with-plugins/internal/controller/memcached_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	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/types"
	"k8s.io/client-go/tools/events"
	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
)

const memcachedFinalizer = "example.com.testproject.org/finalizer"

// Definitions to manage status conditions
const (
	// typeAvailableMemcached represents the status of the Deployment reconciliation
	typeAvailableMemcached = "Available"
	// typeDegradedMemcached represents the status used when the custom resource is deleted and the finalizer operations are yet to occur.
	typeDegradedMemcached = "Degraded"
)

// MemcachedReconciler reconciles a Memcached object
type MemcachedReconciler struct {
	client.Client
	Scheme   *runtime.Scheme
	Recorder events.EventRecorder
}

// The following markers are used to generate the rules permissions (RBAC) on config/rbac using controller-gen
// when the command  is executed.
// To know more about markers see: https://book.kubebuilder.io/reference/markers.html

// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=memcacheds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=memcacheds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,namespace=project-v4-with-plugins-system,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,namespace=project-v4-with-plugins-system,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,namespace=project-v4-with-plugins-system,resources=pods,verbs=get;list;watch

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator
// pattern you will create Controllers which provide a reconcile function
// responsible for synchronizing resources until the desired state is reached on the cluster.
// Breaking this recommendation goes against the design principles of controller-runtime.
// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention.
// For further info:
// - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
// - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := logf.FromContext(ctx)

	// Fetch the Memcached instance
	// The purpose is check if the Custom Resource for the Kind Memcached
	// is applied on the cluster if not we return nil to stop the reconciliation
	memcached := &examplecomv1alpha1.Memcached{}
	err := r.Get(ctx, req.NamespacedName, memcached)
	if err != nil {
		if apierrors.IsNotFound(err) {
			// If the custom resource is not found then it usually means that it was deleted or not created
			// In this way, we will stop the reconciliation
			log.Info("Memcached resource not found, ignoring since object must be deleted")
			return ctrl.Result{}, nil
		}
		// Error reading the object - requeue the request.
		log.Error(err, "Failed to get memcached")
		return ctrl.Result{}, err
	}

	if len(memcached.Status.Conditions) == 0 {
		meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionUnknown, Reason: "Reconciling", Message: "Starting reconciliation"})
		if err = r.Status().Update(ctx, memcached); err != nil {
			log.Error(err, "Failed to update Memcached status")
			return ctrl.Result{}, err
		}

		// Let's re-fetch the memcached Custom Resource after updating the status
		// so that we have the latest state of the resource on the cluster and we will avoid
		// raising the error "the object has been modified, please apply
		// your changes to the latest version and try again" which would re-trigger the reconciliation
		// if we try to update it again in the following operations
		if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
			log.Error(err, "Failed to re-fetch memcached")
			return ctrl.Result{}, err
		}
	}

	// Let's add a finalizer. Then, we can define some operations which should
	// occur before the custom resource is deleted.
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/finalizers
	if !controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) {
		log.Info("Adding finalizer for Memcached")
		controllerutil.AddFinalizer(memcached, memcachedFinalizer)
		if err = r.Update(ctx, memcached); err != nil {
			log.Error(err, "Failed to update custom resource to add finalizer")
			return ctrl.Result{}, err
		}
	}

	// Check if the Memcached instance is marked to be deleted, which is
	// indicated by the deletion timestamp being set.
	isMemcachedMarkedToBeDeleted := memcached.GetDeletionTimestamp() != nil
	if isMemcachedMarkedToBeDeleted {
		if controllerutil.ContainsFinalizer(memcached, memcachedFinalizer) {
			log.Info("Performing finalizer operations for Memcached before deleting CR")

			// Let's add here a status "Downgrade" to reflect that this resource began its process to be terminated.
			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeDegradedMemcached,
				Status: metav1.ConditionUnknown, Reason: "Finalizing",
				Message: fmt.Sprintf("Performing finalizer operations for the custom resource: %s ", memcached.Name)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			// Perform all operations required before removing the finalizer and allow
			// the Kubernetes API to remove the custom resource.
			r.doFinalizerOperationsForMemcached(memcached)

			// TODO(user): If you add operations to the doFinalizerOperationsForMemcached method
			// then you need to ensure that all worked fine before deleting and updating the Downgrade status
			// otherwise, you should requeue here.

			// Re-fetch the memcached Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
				log.Error(err, "Failed to re-fetch memcached")
				return ctrl.Result{}, err
			}

			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeDegradedMemcached,
				Status: metav1.ConditionTrue, Reason: "Finalizing",
				Message: fmt.Sprintf("Finalizer operations for custom resource %s name were successfully accomplished", memcached.Name)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			log.Info("Removing finalizer for Memcached after successfully performing the operations")
			if ok := controllerutil.RemoveFinalizer(memcached, memcachedFinalizer); !ok {
				err = fmt.Errorf("finalizer for Memcached was not removed")
				log.Error(err, "Failed to remove finalizer for Memcached")
				return ctrl.Result{}, err
			}

			if err := r.Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to remove finalizer for Memcached")
				return ctrl.Result{}, err
			}
		}
		return ctrl.Result{}, nil
	}

	// Check if the deployment already exists, if not create a new one
	found := &appsv1.Deployment{}
	err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found)
	if err != nil && apierrors.IsNotFound(err) {
		// Define a new deployment
		dep, err := r.deploymentForMemcached(memcached)
		if err != nil {
			log.Error(err, "Failed to define new Deployment resource for Memcached")

			// The following implementation will update the status
			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
				Status: metav1.ConditionFalse, Reason: "Reconciling",
				Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", memcached.Name, err)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		log.Info("Creating a new Deployment",
			"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
		if err = r.Create(ctx, dep); err != nil {
			log.Error(err, "Failed to create new Deployment",
				"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
			return ctrl.Result{}, err
		}

		// Deployment created successfully
		// We will requeue the reconciliation so that we can ensure the state
		// and move forward for the next operations
		return ctrl.Result{RequeueAfter: time.Minute}, nil
	} else if err != nil {
		log.Error(err, "Failed to get Deployment")
		// Let's return the error for the reconciliation be re-triggered again
		return ctrl.Result{}, err
	}

	// If the size is not defined in the Custom Resource then we will set the desired replicas to 0
	var desiredReplicas int32 = 0
	if memcached.Spec.Size != nil {
		desiredReplicas = *memcached.Spec.Size
	}

	// The CRD API defines that the Memcached type have a MemcachedSpec.Size field
	// to set the quantity of Deployment instances to the desired state on the cluster.
	// Therefore, the following code will ensure the Deployment size is the same as defined
	// via the Size spec of the Custom Resource which we are reconciling.
	if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas {
		found.Spec.Replicas = ptr.To(desiredReplicas)
		if err = r.Update(ctx, found); err != nil {
			log.Error(err, "Failed to update Deployment",
				"Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)

			// Re-fetch the memcached Custom Resource before updating the status
			// so that we have the latest state of the resource on the cluster and we will avoid
			// raising the error "the object has been modified, please apply
			// your changes to the latest version and try again" which would re-trigger the reconciliation
			if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
				log.Error(err, "Failed to re-fetch memcached")
				return ctrl.Result{}, err
			}

			// The following implementation will update the status
			meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
				Status: metav1.ConditionFalse, Reason: "Resizing",
				Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", memcached.Name, err)})

			if err := r.Status().Update(ctx, memcached); err != nil {
				log.Error(err, "Failed to update Memcached status")
				return ctrl.Result{}, err
			}

			return ctrl.Result{}, err
		}

		// Now, that we update the size we want to requeue the reconciliation
		// so that we can ensure that we have the latest state of the resource before
		// update. Also, it will help ensure the desired state on the cluster
		return ctrl.Result{Requeue: true}, nil
	}

	// The following implementation will update the status
	meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
		Status: metav1.ConditionTrue, Reason: "Reconciling",
		Message: fmt.Sprintf("Deployment for custom resource (%s) with %d replicas created successfully", memcached.Name, desiredReplicas)})

	if err := r.Status().Update(ctx, memcached); err != nil {
		log.Error(err, "Failed to update Memcached status")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

// finalizeMemcached will perform the required operations before delete the CR.
func (r *MemcachedReconciler) doFinalizerOperationsForMemcached(cr *examplecomv1alpha1.Memcached) {
	// TODO(user): Add the cleanup steps that the operator
	// needs to do before the CR can be deleted. Examples
	// of finalizers include performing backups and deleting
	// resources that are not owned by this CR, like a PVC.

	// Note: It is not recommended to use finalizers with the purpose of deleting resources which are
	// created and managed in the reconciliation. These ones, such as the Deployment created on this reconcile,
	// are defined as dependent of the custom resource. See that we use the method ctrl.SetControllerReference.
	// to set the ownerRef which means that the Deployment will be deleted by the Kubernetes API.
	// More info: https://kubernetes.io/docs/tasks/administer-cluster/use-cascading-deletion/

	// The following implementation will raise an event
	r.Recorder.Eventf(cr, nil, corev1.EventTypeWarning, "Deleting", "DeleteCR",
		"Custom Resource %s is being deleted from the namespace %s",
		cr.Name,
		cr.Namespace)
}

// deploymentForMemcached returns a Memcached Deployment object
func (r *MemcachedReconciler) deploymentForMemcached(
	memcached *examplecomv1alpha1.Memcached) (*appsv1.Deployment, error) {
	ls := labelsForMemcached()

	// Get the Operand image
	image, err := imageForMemcached()
	if err != nil {
		return nil, err
	}

	dep := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      memcached.Name,
			Namespace: memcached.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Replicas: memcached.Spec.Size,
			Selector: &metav1.LabelSelector{
				MatchLabels: ls,
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{
					Labels: ls,
				},
				Spec: corev1.PodSpec{
					// TODO(user): Uncomment the following code to configure the nodeAffinity expression
					// according to the platforms which are supported by your solution. It is considered
					// best practice to support multiple architectures. build your manager image using the
					// makefile target docker-buildx. Also, you can use docker manifest inspect 
					// to check what are the platforms supported.
					// More info: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity
					// Affinity: &corev1.Affinity{
					//	 NodeAffinity: &corev1.NodeAffinity{
					//		 RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
					//			 NodeSelectorTerms: []corev1.NodeSelectorTerm{
					//				 {
					//					 MatchExpressions: []corev1.NodeSelectorRequirement{
					//						 {
					//							 Key:      "kubernetes.io/arch",
					//							 Operator: "In",
					//							 Values:   []string{"amd64", "arm64", "ppc64le", "s390x"},
					//						 },
					//						 {
					//							 Key:      "kubernetes.io/os",
					//							 Operator: "In",
					//							 Values:   []string{"linux"},
					//						 },
					//					 },
					//				 },
					//		 	 },
					//		 },
					//	 },
					// },
					SecurityContext: &corev1.PodSecurityContext{
						RunAsNonRoot: ptr.To(true),
						// IMPORTANT: seccomProfile was introduced with Kubernetes 1.19
						// If you are looking for to produce solutions to be supported
						// on lower versions you must remove this option.
						SeccompProfile: &corev1.SeccompProfile{
							Type: corev1.SeccompProfileTypeRuntimeDefault,
						},
					},
					Containers: []corev1.Container{{
						Image:           image,
						Name:            "memcached",
						ImagePullPolicy: corev1.PullIfNotPresent,
						// Ensure restrictive context for the container
						// More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
						SecurityContext: &corev1.SecurityContext{
							RunAsNonRoot:             ptr.To(true),
							RunAsUser:                ptr.To(int64(1001)),
							AllowPrivilegeEscalation: ptr.To(false),
							Capabilities: &corev1.Capabilities{
								Drop: []corev1.Capability{
									"ALL",
								},
							},
						},
						Ports: []corev1.ContainerPort{{
							ContainerPort: memcached.Spec.ContainerPort,
							Name:          "memcached",
						}},
						Command: []string{"memcached", "--memory-limit=64", "-o", "modern", "-v"},
					}},
				},
			},
		},
	}

	// Set the ownerRef for the Deployment
	// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/
	if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil {
		return nil, err
	}
	return dep, nil
}

// labelsForMemcached returns the labels for selecting the resources
// More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/
func labelsForMemcached() map[string]string {
	var imageTag string
	image, err := imageForMemcached()
	if err == nil {
		imageTag = strings.Split(image, ":")[1]
	}
	return map[string]string{
		"app.kubernetes.io/name":       "project-v4-with-plugins",
		"app.kubernetes.io/version":    imageTag,
		"app.kubernetes.io/managed-by": "MemcachedController",
	}
}

// imageForMemcached gets the Operand image which is managed by this controller
// from the MEMCACHED_IMAGE environment variable defined in the config/manager/manager.yaml
func imageForMemcached() (string, error) {
	var imageEnvVar = "MEMCACHED_IMAGE"
	image, found := os.LookupEnv(imageEnvVar)
	if !found {
		return "", fmt.Errorf("unable to find %s environment variable with the image", imageEnvVar)
	}
	return image, nil
}

// SetupWithManager sets up the controller with the Manager.
// The whole idea is to be watching the resources that matter for the controller.
// When a resource that the controller is interested in changes, the Watch triggers
// the controller’s reconciliation loop, ensuring that the actual state of the resource
// matches the desired state as defined in the controller’s logic.
//
// Notice how we configured the Manager to monitor events such as the creation, update,
// or deletion of a Custom Resource (CR) of the Memcached kind, as well as any changes
// to the Deployment that the controller manages and owns.
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		// Watch the Memcached CR(s) and trigger reconciliation whenever it
		// is created, updated, or deleted
		For(&examplecomv1alpha1.Memcached{}).
		Named("memcached").
		// Watch the Deployment managed by the MemcachedReconciler. If any changes occur to the Deployment
		// owned and managed by this controller, it will trigger reconciliation, ensuring that the cluster
		// state aligns with the desired state. See that the ownerRef was set when the Deployment was created.
		Owns(&appsv1.Deployment{}).
		Complete(r)
}


================================================
FILE: testdata/project-v4-with-plugins/internal/controller/memcached_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"
	"os"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
)

var _ = Describe("Memcached controller", func() {
	Context("Memcached controller test", func() {

		const MemcachedName = "test-memcached"

		ctx := context.Background()

		namespace := &corev1.Namespace{
			ObjectMeta: metav1.ObjectMeta{
				Name:      MemcachedName,
				Namespace: MemcachedName,
			},
		}

		typeNamespacedName := types.NamespacedName{
			Name:      MemcachedName,
			Namespace: MemcachedName,
		}
		memcached := &examplecomv1alpha1.Memcached{}

		SetDefaultEventuallyTimeout(2 * time.Minute)
		SetDefaultEventuallyPollingInterval(time.Second)

		BeforeEach(func() {
			By("Creating the Namespace to perform the tests")
			err := k8sClient.Create(ctx, namespace)
			Expect(err).NotTo(HaveOccurred())

			By("Setting the Image ENV VAR which stores the Operand image")
			err = os.Setenv("MEMCACHED_IMAGE", "example.com/image:test")
			Expect(err).NotTo(HaveOccurred())

			By("creating the custom resource for the Kind Memcached")
			err = k8sClient.Get(ctx, typeNamespacedName, memcached)
			if err != nil && errors.IsNotFound(err) {
				// Let's mock our custom resource at the same way that we would
				// apply on the cluster the manifest under config/samples
				memcached = &examplecomv1alpha1.Memcached{
					ObjectMeta: metav1.ObjectMeta{
						Name:      MemcachedName,
						Namespace: namespace.Name,
					},
					Spec: examplecomv1alpha1.MemcachedSpec{
						Size:          ptr.To(int32(1)),
						ContainerPort: 11211,
					},
				}

				err = k8sClient.Create(ctx, memcached)
				Expect(err).NotTo(HaveOccurred())
			}
		})

		AfterEach(func() {
			By("removing the custom resource for the Kind Memcached")
			found := &examplecomv1alpha1.Memcached{}
			err := k8sClient.Get(ctx, typeNamespacedName, found)
			Expect(err).NotTo(HaveOccurred())

			Eventually(func(g Gomega) {
				g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed())
			}).Should(Succeed())

			// TODO(user): Attention if you improve this code by adding other context test you MUST
			// be aware of the current delete namespace limitations.
			// More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations
			By("Deleting the Namespace to perform the tests")
			_ = k8sClient.Delete(ctx, namespace)

			By("Removing the Image ENV VAR which stores the Operand image")
			_ = os.Unsetenv("MEMCACHED_IMAGE")
		})

		It("should successfully reconcile a custom resource for Memcached", func() {
			By("Checking if the custom resource was successfully created")
			Eventually(func(g Gomega) {
				found := &examplecomv1alpha1.Memcached{}
				Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource created")
			memcachedReconciler := &MemcachedReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := memcachedReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking if Deployment was successfully created in the reconciliation")
			Eventually(func(g Gomega) {
				found := &appsv1.Deployment{}
				g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
			}).Should(Succeed())

			By("Reconciling the custom resource again")
			_, err = memcachedReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())

			By("Checking the latest Status Condition added to the Memcached instance")
			Expect(k8sClient.Get(ctx, typeNamespacedName, memcached)).To(Succeed())
			var conditions []metav1.Condition
			Expect(memcached.Status.Conditions).To(ContainElement(
				HaveField("Type", Equal(typeAvailableMemcached)), &conditions))
			Expect(conditions).To(HaveLen(1), "Multiple conditions of type %s", typeAvailableMemcached)
			Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "condition %s", typeAvailableMemcached)
			Expect(conditions[0].Reason).To(Equal("Reconciling"), "condition %s", typeAvailableMemcached)
		})
	})
})


================================================
FILE: testdata/project-v4-with-plugins/internal/controller/suite_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	testEnv   *envtest.Environment
	cfg       *rest.Config
	k8sClient client.Client
)

func TestControllers(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Controller Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = examplecomv1alpha1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	err = examplecomv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: true,
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-with-plugins/internal/controller/wordpress_controller.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	"k8s.io/apimachinery/pkg/runtime"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
)

// WordpressReconciler reconciles a Wordpress object
type WordpressReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=wordpresses,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=wordpresses/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=example.com.testproject.org,namespace=project-v4-with-plugins-system,resources=wordpresses/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the Wordpress object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.23.3/pkg/reconcile
func (r *WordpressReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	_ = logf.FromContext(ctx)

	// TODO(user): your logic here

	return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *WordpressReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&examplecomv1.Wordpress{}).
		Named("wordpress").
		Complete(r)
}


================================================
FILE: testdata/project-v4-with-plugins/internal/controller/wordpress_controller_test.go
================================================
/*
Copyright 2026 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 controller

import (
	"context"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
	"k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
)

var _ = Describe("Wordpress Controller", func() {
	Context("When reconciling a resource", func() {
		const resourceName = "test-resource"

		ctx := context.Background()

		typeNamespacedName := types.NamespacedName{
			Name:      resourceName,
			Namespace: "default", // TODO(user):Modify as needed
		}
		wordpress := &examplecomv1.Wordpress{}

		BeforeEach(func() {
			By("creating the custom resource for the Kind Wordpress")
			err := k8sClient.Get(ctx, typeNamespacedName, wordpress)
			if err != nil && errors.IsNotFound(err) {
				resource := &examplecomv1.Wordpress{
					ObjectMeta: metav1.ObjectMeta{
						Name:      resourceName,
						Namespace: "default",
					},
					// TODO(user): Specify other spec details if needed.
				}
				Expect(k8sClient.Create(ctx, resource)).To(Succeed())
			}
		})

		AfterEach(func() {
			// TODO(user): Cleanup logic after each test, like removing the resource instance.
			resource := &examplecomv1.Wordpress{}
			err := k8sClient.Get(ctx, typeNamespacedName, resource)
			Expect(err).NotTo(HaveOccurred())

			By("Cleanup the specific resource instance Wordpress")
			Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
		})
		It("should successfully reconcile the resource", func() {
			By("Reconciling the created resource")
			controllerReconciler := &WordpressReconciler{
				Client: k8sClient,
				Scheme: k8sClient.Scheme(),
			}

			_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
				NamespacedName: typeNamespacedName,
			})
			Expect(err).NotTo(HaveOccurred())
			// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
			// Example: If you expect a certain status condition after reconciliation, verify it here.
		})
	})
})


================================================
FILE: testdata/project-v4-with-plugins/internal/webhook/v1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = examplecomv1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupWordpressWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-with-plugins/internal/webhook/v1/wordpress_webhook.go
================================================
/*
Copyright 2026 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 v1

import (
	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
)

// nolint:unused
// log is for logging in this package.
var wordpresslog = logf.Log.WithName("wordpress-resource")

// SetupWordpressWebhookWithManager registers the webhook for Wordpress in the manager.
func SetupWordpressWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &examplecomv1.Wordpress{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!


================================================
FILE: testdata/project-v4-with-plugins/internal/webhook/v1/wordpress_webhook_test.go
================================================
/*
Copyright 2026 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 v1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	examplecomv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Wordpress Webhook", func() {
	var (
		obj    *examplecomv1.Wordpress
		oldObj *examplecomv1.Wordpress
	)

	BeforeEach(func() {
		obj = &examplecomv1.Wordpress{}
		oldObj = &examplecomv1.Wordpress{}
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating Wordpress under Conversion Webhook", func() {
		// TODO (user): Add logic to convert the object to the desired version and verify the conversion
		// Example:
		// It("Should convert the object correctly", func() {
		//     convertedObj := &examplecomv1.Wordpress{}
		//     Expect(obj.ConvertTo(convertedObj)).To(Succeed())
		//     Expect(convertedObj).ToNot(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	"context"

	ctrl "sigs.k8s.io/controller-runtime"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
)

// nolint:unused
// log is for logging in this package.
var memcachedlog = logf.Log.WithName("memcached-resource")

// SetupMemcachedWebhookWithManager registers the webhook for Memcached in the manager.
func SetupMemcachedWebhookWithManager(mgr ctrl.Manager) error {
	return ctrl.NewWebhookManagedBy(mgr, &examplecomv1alpha1.Memcached{}).
		WithValidator(&MemcachedCustomValidator{}).
		Complete()
}

// TODO(user): EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-example-com-testproject-org-v1alpha1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=example.com.testproject.org,resources=memcacheds,verbs=create;update,versions=v1alpha1,name=vmemcached-v1alpha1.kb.io,admissionReviewVersions=v1

// MemcachedCustomValidator struct is responsible for validating the Memcached resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type MemcachedCustomValidator struct {
	// TODO(user): Add more fields as needed for validation
}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Memcached.
func (v *MemcachedCustomValidator) ValidateCreate(_ context.Context, obj *examplecomv1alpha1.Memcached) (admission.Warnings, error) {
	memcachedlog.Info("Validation for Memcached upon creation", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object creation.

	return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Memcached.
func (v *MemcachedCustomValidator) ValidateUpdate(_ context.Context, oldObj, newObj *examplecomv1alpha1.Memcached) (admission.Warnings, error) {
	memcachedlog.Info("Validation for Memcached upon update", "name", newObj.GetName())

	// TODO(user): fill in your validation logic upon object update.

	return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Memcached.
func (v *MemcachedCustomValidator) ValidateDelete(_ context.Context, obj *examplecomv1alpha1.Memcached) (admission.Warnings, error) {
	memcachedlog.Info("Validation for Memcached upon deletion", "name", obj.GetName())

	// TODO(user): fill in your validation logic upon object deletion.

	return nil, nil
}


================================================
FILE: testdata/project-v4-with-plugins/internal/webhook/v1alpha1/memcached_webhook_test.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
	// TODO (user): Add any additional imports if needed
)

var _ = Describe("Memcached Webhook", func() {
	var (
		obj       *examplecomv1alpha1.Memcached
		oldObj    *examplecomv1alpha1.Memcached
		validator MemcachedCustomValidator
	)

	BeforeEach(func() {
		obj = &examplecomv1alpha1.Memcached{}
		oldObj = &examplecomv1alpha1.Memcached{}
		validator = MemcachedCustomValidator{}
		Expect(validator).NotTo(BeNil(), "Expected validator to be initialized")
		Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
		Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
	})

	AfterEach(func() {
		// TODO (user): Add any teardown logic common to all tests
	})

	Context("When creating or updating Memcached under Validating Webhook", func() {
		// TODO (user): Add logic for validating webhooks
		// Example:
		// It("Should deny creation if a required field is missing", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = ""
		//     Expect(validator.ValidateCreate(ctx, obj)).Error().To(HaveOccurred())
		// })
		//
		// It("Should admit creation if all required fields are present", func() {
		//     By("simulating an invalid creation scenario")
		//     obj.SomeRequiredField = "valid_value"
		//     Expect(validator.ValidateCreate(ctx, obj)).To(BeNil())
		// })
		//
		// It("Should validate updates correctly", func() {
		//     By("simulating a valid update scenario")
		//     oldObj.SomeRequiredField = "updated_value"
		//     obj.SomeRequiredField = "updated_value"
		//     Expect(validator.ValidateUpdate(ctx, oldObj, obj)).To(BeNil())
		// })
	})

})


================================================
FILE: testdata/project-v4-with-plugins/internal/webhook/v1alpha1/webhook_suite_test.go
================================================
/*
Copyright 2026 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 v1alpha1

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"os"
	"path/filepath"
	"testing"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/envtest"
	logf "sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/log/zap"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
	"sigs.k8s.io/controller-runtime/pkg/webhook"

	examplecomv1alpha1 "sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/api/v1alpha1"
	// +kubebuilder:scaffold:imports
)

// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.

var (
	ctx       context.Context
	cancel    context.CancelFunc
	k8sClient client.Client
	cfg       *rest.Config
	testEnv   *envtest.Environment
)

func TestAPIs(t *testing.T) {
	RegisterFailHandler(Fail)

	RunSpecs(t, "Webhook Suite")
}

var _ = BeforeSuite(func() {
	logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))

	ctx, cancel = context.WithCancel(context.TODO())

	var err error
	err = examplecomv1alpha1.AddToScheme(scheme.Scheme)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:scheme

	By("bootstrapping test environment")
	testEnv = &envtest.Environment{
		CRDDirectoryPaths:     []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
		ErrorIfCRDPathMissing: false,

		WebhookInstallOptions: envtest.WebhookInstallOptions{
			Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")},
		},
	}

	// Retrieve the first found binary directory to allow running tests from IDEs
	if getFirstFoundEnvTestBinaryDir() != "" {
		testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
	}

	// cfg is defined in this file globally.
	cfg, err = testEnv.Start()
	Expect(err).NotTo(HaveOccurred())
	Expect(cfg).NotTo(BeNil())

	k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
	Expect(err).NotTo(HaveOccurred())
	Expect(k8sClient).NotTo(BeNil())

	// start webhook server using Manager.
	webhookInstallOptions := &testEnv.WebhookInstallOptions
	mgr, err := ctrl.NewManager(cfg, ctrl.Options{
		Scheme: scheme.Scheme,
		WebhookServer: webhook.NewServer(webhook.Options{
			Host:    webhookInstallOptions.LocalServingHost,
			Port:    webhookInstallOptions.LocalServingPort,
			CertDir: webhookInstallOptions.LocalServingCertDir,
		}),
		LeaderElection: false,
		Metrics:        metricsserver.Options{BindAddress: "0"},
	})
	Expect(err).NotTo(HaveOccurred())

	err = SetupMemcachedWebhookWithManager(mgr)
	Expect(err).NotTo(HaveOccurred())

	// +kubebuilder:scaffold:webhook

	go func() {
		defer GinkgoRecover()
		err = mgr.Start(ctx)
		Expect(err).NotTo(HaveOccurred())
	}()

	// wait for the webhook server to get ready.
	dialer := &net.Dialer{Timeout: time.Second}
	addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort)
	Eventually(func() error {
		conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true})
		if err != nil {
			return err
		}

		return conn.Close()
	}).Should(Succeed())
})

var _ = AfterSuite(func() {
	By("tearing down the test environment")
	cancel()
	Eventually(func() error {
		return testEnv.Stop()
	}, time.Minute, time.Second).Should(Succeed())
})

// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
	basePath := filepath.Join("..", "..", "..", "bin", "k8s")
	entries, err := os.ReadDir(basePath)
	if err != nil {
		logf.Log.Error(err, "Failed to read directory", "path", basePath)
		return ""
	}
	for _, entry := range entries {
		if entry.IsDir() {
			return filepath.Join(basePath, entry.Name())
		}
	}
	return ""
}


================================================
FILE: testdata/project-v4-with-plugins/test/e2e/e2e_suite_test.go
================================================
//go:build e2e
// +build e2e

/*
Copyright 2026 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 e2e

import (
	"fmt"
	"os"
	"os/exec"
	"testing"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/test/utils"
)

var (
	// managerImage is the manager image to be built and loaded for testing.
	managerImage = "example.com/project-v4-with-plugins:v0.0.1"
	// shouldCleanupCertManager tracks whether CertManager was installed by this suite.
	shouldCleanupCertManager = false
)

// TestE2E runs the e2e test suite to validate the solution in an isolated environment.
// The default setup requires Kind and CertManager.
//
// To skip CertManager installation, set: CERT_MANAGER_INSTALL_SKIP=true
func TestE2E(t *testing.T) {
	RegisterFailHandler(Fail)
	_, _ = fmt.Fprintf(GinkgoWriter, "Starting project-v4-with-plugins e2e test suite\n")
	RunSpecs(t, "e2e suite")
}

var _ = BeforeSuite(func() {
	By("building the manager image")
	cmd := exec.Command("make", "docker-build", fmt.Sprintf("IMG=%s", managerImage))
	_, err := utils.Run(cmd)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to build the manager image")

	// TODO(user): If you want to change the e2e test vendor from Kind,
	// ensure the image is built and available, then remove the following block.
	By("loading the manager image on Kind")
	err = utils.LoadImageToKindClusterWithName(managerImage)
	ExpectWithOffset(1, err).NotTo(HaveOccurred(), "Failed to load the manager image into Kind")

	setupCertManager()
})

var _ = AfterSuite(func() {
	teardownCertManager()
})

// setupCertManager installs CertManager if needed for webhook tests.
// Skips installation if CERT_MANAGER_INSTALL_SKIP=true or if already present.
func setupCertManager() {
	if os.Getenv("CERT_MANAGER_INSTALL_SKIP") == "true" {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager installation (CERT_MANAGER_INSTALL_SKIP=true)\n")
		return
	}

	By("checking if CertManager is already installed")
	if utils.IsCertManagerCRDsInstalled() {
		_, _ = fmt.Fprintf(GinkgoWriter, "CertManager is already installed. Skipping installation.\n")
		return
	}

	// Mark for cleanup before installation to handle interruptions and partial installs.
	shouldCleanupCertManager = true

	By("installing CertManager")
	Expect(utils.InstallCertManager()).To(Succeed(), "Failed to install CertManager")
}

// teardownCertManager uninstalls CertManager if it was installed by setupCertManager.
// This ensures we only remove what we installed.
func teardownCertManager() {
	if !shouldCleanupCertManager {
		_, _ = fmt.Fprintf(GinkgoWriter, "Skipping CertManager cleanup (not installed by this suite)\n")
		return
	}

	By("uninstalling CertManager")
	utils.UninstallCertManager()
}


================================================
FILE: testdata/project-v4-with-plugins/test/e2e/e2e_test.go
================================================
//go:build e2e
// +build e2e

/*
Copyright 2026 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 e2e

import (
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"time"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"

	"sigs.k8s.io/kubebuilder/testdata/project-v4-with-plugins/test/utils"
)

// namespace where the project is deployed in
const namespace = "project-v4-with-plugins-system"

// serviceAccountName created for the project
const serviceAccountName = "project-v4-with-plugins-controller-manager"

// metricsServiceName is the name of the metrics service of the project
const metricsServiceName = "project-v4-with-plugins-controller-manager-metrics-service"

// metricsRoleBindingName is the name of the RBAC that will be created to allow get the metrics data
const metricsRoleBindingName = "project-v4-with-plugins-metrics-binding"

var _ = Describe("Manager", Ordered, func() {
	var controllerPodName string

	// Before running the tests, set up the environment by creating the namespace,
	// enforce the restricted security policy to the namespace, installing CRDs,
	// and deploying the controller.
	BeforeAll(func() {
		By("creating manager namespace")
		cmd := exec.Command("kubectl", "create", "ns", namespace)
		_, err := utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to create namespace")

		By("labeling the namespace to enforce the restricted security policy")
		cmd = exec.Command("kubectl", "label", "--overwrite", "ns", namespace,
			"pod-security.kubernetes.io/enforce=restricted")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to label namespace with restricted policy")

		By("installing CRDs")
		cmd = exec.Command("make", "install")
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to install CRDs")

		By("deploying the controller-manager")
		cmd = exec.Command("make", "deploy", fmt.Sprintf("IMG=%s", managerImage))
		_, err = utils.Run(cmd)
		Expect(err).NotTo(HaveOccurred(), "Failed to deploy the controller-manager")
	})

	// After all tests have been executed, clean up by undeploying the controller, uninstalling CRDs,
	// and deleting the namespace.
	AfterAll(func() {
		By("cleaning up the curl pod for metrics")
		cmd := exec.Command("kubectl", "delete", "pod", "curl-metrics", "-n", namespace)
		_, _ = utils.Run(cmd)

		By("undeploying the controller-manager")
		cmd = exec.Command("make", "undeploy")
		_, _ = utils.Run(cmd)

		By("uninstalling CRDs")
		cmd = exec.Command("make", "uninstall")
		_, _ = utils.Run(cmd)

		By("removing manager namespace")
		cmd = exec.Command("kubectl", "delete", "ns", namespace)
		_, _ = utils.Run(cmd)
	})

	// After each test, check for failures and collect logs, events,
	// and pod descriptions for debugging.
	AfterEach(func() {
		specReport := CurrentSpecReport()
		if specReport.Failed() {
			By("Fetching controller manager pod logs")
			cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
			controllerLogs, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Controller logs:\n %s", controllerLogs)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Controller logs: %s", err)
			}

			By("Fetching Kubernetes events")
			cmd = exec.Command("kubectl", "get", "events", "-n", namespace, "--sort-by=.lastTimestamp")
			eventsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Kubernetes events:\n%s", eventsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get Kubernetes events: %s", err)
			}

			By("Fetching curl-metrics logs")
			cmd = exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
			metricsOutput, err := utils.Run(cmd)
			if err == nil {
				_, _ = fmt.Fprintf(GinkgoWriter, "Metrics logs:\n %s", metricsOutput)
			} else {
				_, _ = fmt.Fprintf(GinkgoWriter, "Failed to get curl-metrics logs: %s", err)
			}

			By("Fetching controller manager pod description")
			cmd = exec.Command("kubectl", "describe", "pod", controllerPodName, "-n", namespace)
			podDescription, err := utils.Run(cmd)
			if err == nil {
				fmt.Println("Pod description:\n", podDescription)
			} else {
				fmt.Println("Failed to describe controller pod")
			}
		}
	})

	SetDefaultEventuallyTimeout(2 * time.Minute)
	SetDefaultEventuallyPollingInterval(time.Second)

	Context("Manager", func() {
		It("should run successfully", func() {
			By("validating that the controller-manager pod is running as expected")
			verifyControllerUp := func(g Gomega) {
				// Get the name of the controller-manager pod
				cmd := exec.Command("kubectl", "get",
					"pods", "-l", "control-plane=controller-manager",
					"-o", "go-template={{ range .items }}"+
						"{{ if not .metadata.deletionTimestamp }}"+
						"{{ .metadata.name }}"+
						"{{ \"\\n\" }}{{ end }}{{ end }}",
					"-n", namespace,
				)

				podOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve controller-manager pod information")
				podNames := utils.GetNonEmptyLines(podOutput)
				g.Expect(podNames).To(HaveLen(1), "expected 1 controller pod running")
				controllerPodName = podNames[0]
				g.Expect(controllerPodName).To(ContainSubstring("controller-manager"))

				// Validate the pod's status
				cmd = exec.Command("kubectl", "get",
					"pods", controllerPodName, "-o", "jsonpath={.status.phase}",
					"-n", namespace,
				)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Running"), "Incorrect controller-manager pod status")
			}
			Eventually(verifyControllerUp).Should(Succeed())
		})

		It("should ensure the metrics endpoint is serving metrics", func() {
			By("creating a ClusterRoleBinding for the service account to allow access to metrics")
			cmd := exec.Command("kubectl", "create", "clusterrolebinding", metricsRoleBindingName,
				"--clusterrole=project-v4-with-plugins-metrics-reader",
				fmt.Sprintf("--serviceaccount=%s:%s", namespace, serviceAccountName),
			)
			_, err := utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create ClusterRoleBinding")

			By("validating that the metrics service is available")
			cmd = exec.Command("kubectl", "get", "service", metricsServiceName, "-n", namespace)
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Metrics service should exist")

			By("getting the service account token")
			token, err := serviceAccountToken()
			Expect(err).NotTo(HaveOccurred())
			Expect(token).NotTo(BeEmpty())

			By("ensuring the controller pod is ready")
			verifyControllerPodReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pod", controllerPodName, "-n", namespace,
					"-o", "jsonpath={.status.conditions[?(@.type=='Ready')].status}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("True"), "Controller pod not ready")
			}
			Eventually(verifyControllerPodReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying that the controller manager is serving the metrics server")
			verifyMetricsServerStarted := func(g Gomega) {
				cmd := exec.Command("kubectl", "logs", controllerPodName, "-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(ContainSubstring("Serving metrics server"),
					"Metrics server not yet started")
			}
			Eventually(verifyMetricsServerStarted, 3*time.Minute, time.Second).Should(Succeed())

			By("waiting for the webhook service endpoints to be ready")
			verifyWebhookEndpointsReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "endpointslices.discovery.k8s.io", "-n", namespace,
					"-l", "kubernetes.io/service-name=project-v4-with-plugins-webhook-service",
					"-o", "jsonpath={range .items[*]}{range .endpoints[*]}{.addresses[*]}{end}{end}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "Webhook endpoints should exist")
				g.Expect(output).ShouldNot(BeEmpty(), "Webhook endpoints not yet ready")
			}
			Eventually(verifyWebhookEndpointsReady, 3*time.Minute, time.Second).Should(Succeed())

			By("verifying the validating webhook server is ready")
			verifyValidatingWebhookReady := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "validatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-with-plugins-validating-webhook-configuration",
					"-o", "jsonpath={.webhooks[0].clientConfig.caBundle}")
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred(), "ValidatingWebhookConfiguration should exist")
				g.Expect(output).ShouldNot(BeEmpty(), "Validating webhook CA bundle not yet injected")
			}
			Eventually(verifyValidatingWebhookReady, 3*time.Minute, time.Second).Should(Succeed())

			By("waiting additional time for webhook server to stabilize")
			time.Sleep(5 * time.Second)

			// +kubebuilder:scaffold:e2e-metrics-webhooks-readiness

			By("creating the curl-metrics pod to access the metrics endpoint")
			cmd = exec.Command("kubectl", "run", "curl-metrics", "--restart=Never",
				"--namespace", namespace,
				"--image=curlimages/curl:latest",
				"--overrides",
				fmt.Sprintf(`{
					"spec": {
						"containers": [{
							"name": "curl",
							"image": "curlimages/curl:latest",
							"command": ["/bin/sh", "-c"],
							"args": [
								"for i in $(seq 1 30); do curl -v -k -H 'Authorization: Bearer %s' https://%s.%s.svc.cluster.local:8443/metrics && exit 0 || sleep 2; done; exit 1"
							],
							"securityContext": {
								"readOnlyRootFilesystem": true,
								"allowPrivilegeEscalation": false,
								"capabilities": {
									"drop": ["ALL"]
								},
								"runAsNonRoot": true,
								"runAsUser": 1000,
								"seccompProfile": {
									"type": "RuntimeDefault"
								}
							}
						}],
						"serviceAccountName": "%s"
					}
				}`, token, metricsServiceName, namespace, serviceAccountName))
			_, err = utils.Run(cmd)
			Expect(err).NotTo(HaveOccurred(), "Failed to create curl-metrics pod")

			By("waiting for the curl-metrics pod to complete.")
			verifyCurlUp := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "pods", "curl-metrics",
					"-o", "jsonpath={.status.phase}",
					"-n", namespace)
				output, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(output).To(Equal("Succeeded"), "curl pod in wrong status")
			}
			Eventually(verifyCurlUp, 5*time.Minute).Should(Succeed())

			By("getting the metrics by checking curl-metrics logs")
			verifyMetricsAvailable := func(g Gomega) {
				metricsOutput, err := getMetricsOutput()
				g.Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
				g.Expect(metricsOutput).NotTo(BeEmpty())
				g.Expect(metricsOutput).To(ContainSubstring("< HTTP/1.1 200 OK"))
			}
			Eventually(verifyMetricsAvailable, 2*time.Minute).Should(Succeed())
		})

		It("should provisioned cert-manager", func() {
			By("validating that cert-manager has the certificate Secret")
			verifyCertManager := func(g Gomega) {
				cmd := exec.Command("kubectl", "get", "secrets", "webhook-server-cert", "-n", namespace)
				_, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
			}
			Eventually(verifyCertManager).Should(Succeed())
		})

		It("should have CA injection for validating webhooks", func() {
			By("checking CA injection for validating webhooks")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"validatingwebhookconfigurations.admissionregistration.k8s.io",
					"project-v4-with-plugins-validating-webhook-configuration",
					"-o", "go-template={{ range .webhooks }}{{ .clientConfig.caBundle }}{{ end }}")
				vwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		It("should have CA injection for Wordpress conversion webhook", func() {
			By("checking CA injection for Wordpress conversion webhook")
			verifyCAInjection := func(g Gomega) {
				cmd := exec.Command("kubectl", "get",
					"customresourcedefinitions.apiextensions.k8s.io",
					"wordpresses.example.com.testproject.org",
					"-o", "go-template={{ .spec.conversion.webhook.clientConfig.caBundle }}")
				vwhOutput, err := utils.Run(cmd)
				g.Expect(err).NotTo(HaveOccurred())
				g.Expect(len(vwhOutput)).To(BeNumerically(">", 10))
			}
			Eventually(verifyCAInjection).Should(Succeed())
		})

		// +kubebuilder:scaffold:e2e-webhooks-checks

		// TODO: Customize the e2e test suite with scenarios specific to your project.
		// Consider applying sample/CR(s) and check their status and/or verifying
		// the reconciliation by using the metrics, i.e.:
		// metricsOutput, err := getMetricsOutput()
		// Expect(err).NotTo(HaveOccurred(), "Failed to retrieve logs from curl pod")
		// Expect(metricsOutput).To(ContainSubstring(
		//    fmt.Sprintf(`controller_runtime_reconcile_total{controller="%s",result="success"} 1`,
		//    strings.ToLower(),
		// ))
	})
})

// serviceAccountToken returns a token for the specified service account in the given namespace.
// It uses the Kubernetes TokenRequest API to generate a token by directly sending a request
// and parsing the resulting token from the API response.
func serviceAccountToken() (string, error) {
	const tokenRequestRawString = `{
		"apiVersion": "authentication.k8s.io/v1",
		"kind": "TokenRequest"
	}`

	// Temporary file to store the token request
	secretName := fmt.Sprintf("%s-token-request", serviceAccountName)
	tokenRequestFile := filepath.Join("/tmp", secretName)
	err := os.WriteFile(tokenRequestFile, []byte(tokenRequestRawString), os.FileMode(0o644))
	if err != nil {
		return "", err
	}

	var out string
	verifyTokenCreation := func(g Gomega) {
		// Execute kubectl command to create the token
		cmd := exec.Command("kubectl", "create", "--raw", fmt.Sprintf(
			"/api/v1/namespaces/%s/serviceaccounts/%s/token",
			namespace,
			serviceAccountName,
		), "-f", tokenRequestFile)

		output, err := cmd.CombinedOutput()
		g.Expect(err).NotTo(HaveOccurred())

		// Parse the JSON output to extract the token
		var token tokenRequest
		err = json.Unmarshal(output, &token)
		g.Expect(err).NotTo(HaveOccurred())

		out = token.Status.Token
	}
	Eventually(verifyTokenCreation).Should(Succeed())

	return out, err
}

// getMetricsOutput retrieves and returns the logs from the curl pod used to access the metrics endpoint.
func getMetricsOutput() (string, error) {
	By("getting the curl-metrics logs")
	cmd := exec.Command("kubectl", "logs", "curl-metrics", "-n", namespace)
	return utils.Run(cmd)
}

// tokenRequest is a simplified representation of the Kubernetes TokenRequest API response,
// containing only the token field that we need to extract.
type tokenRequest struct {
	Status struct {
		Token string `json:"token"`
	} `json:"status"`
}


================================================
FILE: testdata/project-v4-with-plugins/test/utils/utils.go
================================================
/*
Copyright 2026 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 utils

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"strings"

	. "github.com/onsi/ginkgo/v2" // nolint:revive,staticcheck
)

const (
	certmanagerVersion = "v1.20.0"
	certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"

	defaultKindBinary  = "kind"
	defaultKindCluster = "kind"
)

func warnError(err error) {
	_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
}

// Run executes the provided command within this context
func Run(cmd *exec.Cmd) (string, error) {
	dir, _ := GetProjectDir()
	cmd.Dir = dir

	if err := os.Chdir(cmd.Dir); err != nil {
		_, _ = fmt.Fprintf(GinkgoWriter, "chdir dir: %q\n", err)
	}

	cmd.Env = append(os.Environ(), "GO111MODULE=on")
	command := strings.Join(cmd.Args, " ")
	_, _ = fmt.Fprintf(GinkgoWriter, "running: %q\n", command)
	output, err := cmd.CombinedOutput()
	if err != nil {
		return string(output), fmt.Errorf("%q failed with error %q: %w", command, string(output), err)
	}

	return string(output), nil
}

// UninstallCertManager uninstalls the cert manager
func UninstallCertManager() {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "delete", "-f", url)
	if _, err := Run(cmd); err != nil {
		warnError(err)
	}

	// Delete leftover leases in kube-system (not cleaned by default)
	kubeSystemLeases := []string{
		"cert-manager-cainjector-leader-election",
		"cert-manager-controller",
	}
	for _, lease := range kubeSystemLeases {
		cmd = exec.Command("kubectl", "delete", "lease", lease,
			"-n", "kube-system", "--ignore-not-found", "--force", "--grace-period=0")
		if _, err := Run(cmd); err != nil {
			warnError(err)
		}
	}
}

// InstallCertManager installs the cert manager bundle.
func InstallCertManager() error {
	url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
	cmd := exec.Command("kubectl", "apply", "-f", url)
	if _, err := Run(cmd); err != nil {
		return err
	}
	// Wait for cert-manager-webhook to be ready, which can take time if cert-manager
	// was re-installed after uninstalling on a cluster.
	cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
		"--for", "condition=Available",
		"--namespace", "cert-manager",
		"--timeout", "5m",
	)

	_, err := Run(cmd)
	return err
}

// IsCertManagerCRDsInstalled checks if any Cert Manager CRDs are installed
// by verifying the existence of key CRDs related to Cert Manager.
func IsCertManagerCRDsInstalled() bool {
	// List of common Cert Manager CRDs
	certManagerCRDs := []string{
		"certificates.cert-manager.io",
		"issuers.cert-manager.io",
		"clusterissuers.cert-manager.io",
		"certificaterequests.cert-manager.io",
		"orders.acme.cert-manager.io",
		"challenges.acme.cert-manager.io",
	}

	// Execute the kubectl command to get all CRDs
	cmd := exec.Command("kubectl", "get", "crds")
	output, err := Run(cmd)
	if err != nil {
		return false
	}

	// Check if any of the Cert Manager CRDs are present
	crdList := GetNonEmptyLines(output)
	for _, crd := range certManagerCRDs {
		for _, line := range crdList {
			if strings.Contains(line, crd) {
				return true
			}
		}
	}

	return false
}

// LoadImageToKindClusterWithName loads a local docker image to the kind cluster
func LoadImageToKindClusterWithName(name string) error {
	cluster := defaultKindCluster
	if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
		cluster = v
	}
	kindOptions := []string{"load", "docker-image", name, "--name", cluster}
	kindBinary := defaultKindBinary
	if v, ok := os.LookupEnv("KIND"); ok {
		kindBinary = v
	}
	cmd := exec.Command(kindBinary, kindOptions...)
	_, err := Run(cmd)
	return err
}

// GetNonEmptyLines converts given command output string into individual objects
// according to line breakers, and ignores the empty elements in it.
func GetNonEmptyLines(output string) []string {
	var res []string
	elements := strings.SplitSeq(output, "\n")
	for element := range elements {
		if element != "" {
			res = append(res, element)
		}
	}

	return res
}

// GetProjectDir will return the directory where the project is
func GetProjectDir() (string, error) {
	wd, err := os.Getwd()
	if err != nil {
		return wd, fmt.Errorf("failed to get current working directory: %w", err)
	}
	wd = strings.ReplaceAll(wd, "/test/e2e", "")
	return wd, nil
}

// UncommentCode searches for target in the file and remove the comment prefix
// of the target content. The target content may span multiple lines.
func UncommentCode(filename, target, prefix string) error {
	// false positive
	// nolint:gosec
	content, err := os.ReadFile(filename)
	if err != nil {
		return fmt.Errorf("failed to read file %q: %w", filename, err)
	}
	strContent := string(content)

	idx := strings.Index(strContent, target)
	if idx < 0 {
		return fmt.Errorf("unable to find the code %q to be uncommented", target)
	}

	out := new(bytes.Buffer)
	_, err = out.Write(content[:idx])
	if err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	scanner := bufio.NewScanner(bytes.NewBufferString(target))
	if !scanner.Scan() {
		return nil
	}
	for {
		if _, err = out.WriteString(strings.TrimPrefix(scanner.Text(), prefix)); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
		// Avoid writing a newline in case the previous line was the last in target.
		if !scanner.Scan() {
			break
		}
		if _, err = out.WriteString("\n"); err != nil {
			return fmt.Errorf("failed to write to output: %w", err)
		}
	}

	if _, err = out.Write(content[idx+len(target):]); err != nil {
		return fmt.Errorf("failed to write to output: %w", err)
	}

	// false positive
	// nolint:gosec
	if err = os.WriteFile(filename, out.Bytes(), 0644); err != nil {
		return fmt.Errorf("failed to write file %q: %w", filename, err)
	}

	return nil
}