diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 7c33becbf9e..115e442685c 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -67,6 +67,7 @@ - [Generating CRDs](./reference/generating-crd.md) - [Using Finalizers](./reference/using-finalizers.md) - [Good Practices](./reference/good-practices.md) + - [Server-Side Apply](./reference/server-side-apply.md) - [License Header](./reference/license-header.md) - [Raising Events](./reference/raising-events.md) - [Watching Resources](./reference/watching-resources.md) diff --git a/docs/book/src/faq.md b/docs/book/src/faq.md index 21430e0f2f2..29331360102 100644 --- a/docs/book/src/faq.md +++ b/docs/book/src/faq.md @@ -105,9 +105,7 @@ However, note that this problem is fixed and will not occur if you deploy the pr 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 does not have to be stored as an annotation (last-applied-configuration) on the server. However, it is worth noting that as of now, it is not 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: +You have a few options to work around this scenario such as: **By removing the descriptions from CRDs:** @@ -127,6 +125,24 @@ Therefore, you have a few options to workround this scenario such as: 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. +**By using Server-Side Apply for CRD installation:** + +Since this issue happens when CRDs are installed with client-side apply, another option is to install or update the CRDs with Server-Side Apply instead, for example by using `kubectl apply --server-side -f ...`. This avoids storing the full manifest in the `kubectl.kubernetes.io/last-applied-configuration` annotation and can prevent the size-limit failure. + +If you are updating existing CRDs, `kubectl replace -f ...` may also help, depending on your workflow. + + + ## 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. @@ -168,4 +184,4 @@ type StructName struct { [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 \ No newline at end of file +[k8s-ssa-docs]: https://kubernetes.io/docs/reference/using-api/server-side-apply/ \ No newline at end of file diff --git a/docs/book/src/reference/server-side-apply.md b/docs/book/src/reference/server-side-apply.md new file mode 100644 index 00000000000..fd99544b046 --- /dev/null +++ b/docs/book/src/reference/server-side-apply.md @@ -0,0 +1,99 @@ +# Server-Side Apply + +The `--ssa` flag scaffolds APIs with [Server-Side Apply][server-side-apply] support, enabling safer field management when multiple actors modify the same resources. + +This adds: +- API markers (`+genclient`, `+kubebuilder:ac:generate=true`) for ApplyConfiguration generation +- Makefile integration to generate type-safe ApplyConfiguration types alongside DeepCopy methods + +## When to use it + +Use Server-Side Apply when: +- Multiple controllers or users manage the same resource +- Users customize CRs with labels, annotations, or spec fields your controller shouldn't overwrite +- You want declarative field ownership tracking +- Other operators will manage instances of your CRs (they can import your generated ApplyConfiguration types) + + + +## How it works + +Traditional `Update()` overwrites the entire object. Server-Side Apply with `client.Apply()` only manages the fields you specify. + +After running `make generate`, ApplyConfiguration types are created at: +``` +api///applyconfiguration/// +``` + +Import them as: +```go +appsv1apply "example.com/myproject/api/apps/v1/applyconfiguration/apps/v1" +``` + +## Usage + +Create an API with the `--ssa` flag: + +```shell +kubebuilder create api --group apps --version v1 --kind Application --ssa +``` + +Implement Server-Side Apply in your controller: + +```go +import ( + "context" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1apply "example.com/myproject/api/apps/v1/applyconfiguration/apps/v1" +) + +func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + // Build desired state - specify only fields you manage + app := appsv1apply.Application(req.Name, req.Namespace). + WithStatus(appsv1apply.ApplicationStatus(). + WithConditions(metav1.Condition{ + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "Reconciled", + })) + + // Apply status + if err := r.SubResource("status").Apply(ctx, app, client.FieldOwner("application-controller")); err != nil { + if apierrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, err + } + return ctrl.Result{}, nil +} +``` + +Then run: +```shell +make generate manifests +``` + + +## Best practices + +- Always specify `client.FieldOwner("controller-name")` to identify your controller +- Handle conflicts by checking `apierrors.IsConflict(err)` and requeueing +- Only specify fields your controller manages using the builder pattern +- Use `client.ForceOwnership` only when your controller must have final authority + +## Additional resources + +- [Kubernetes Server-Side Apply Documentation][server-side-apply] +- [controller-gen CLI Reference](./controller-gen.md) + +[server-side-apply]: https://kubernetes.io/docs/reference/using-api/server-side-apply/ diff --git a/internal/cli/alpha/internal/generate.go b/internal/cli/alpha/internal/generate.go index dae6e9c4186..5c3ae8d9472 100644 --- a/internal/cli/alpha/internal/generate.go +++ b/internal/cli/alpha/internal/generate.go @@ -637,6 +637,10 @@ func getAPIResourceFlags(res resource.Resource) []string { } else { args = append(args, "--namespaced=false") } + // Add --ssa flag if Server-Side Apply is enabled + if res.API.SSA { + args = append(args, "--ssa") + } } // Always disable controller creation in the API scaffolding step diff --git a/pkg/model/resource/api.go b/pkg/model/resource/api.go index 002205161ca..e01b409dfe3 100644 --- a/pkg/model/resource/api.go +++ b/pkg/model/resource/api.go @@ -27,6 +27,11 @@ type API struct { // Namespaced is true if the API is namespaced. Namespaced bool `json:"namespaced,omitempty"` + + // SSA is true if the API should use Server-Side Apply. + // When enabled, the API is scaffolded with +genclient and +kubebuilder:ac:generate=true markers + // for applyconfiguration generation. + SSA bool `json:"ssa,omitempty"` } // Validate checks that the API is valid. @@ -65,10 +70,13 @@ func (api *API) Update(other *API) error { // Update the namespace. api.Namespaced = api.Namespaced || other.Namespaced + // Update SSA. + api.SSA = api.SSA || other.SSA + return nil } // IsEmpty returns if the API's fields all contain zero-values. func (api API) IsEmpty() bool { - return api.CRDVersion == "" && !api.Namespaced + return api.CRDVersion == "" && !api.Namespaced && !api.SSA } diff --git a/pkg/plugins/golang/options.go b/pkg/plugins/golang/options.go index 34dd3b2c529..7ce0afc492b 100644 --- a/pkg/plugins/golang/options.go +++ b/pkg/plugins/golang/options.go @@ -70,6 +70,9 @@ type Options struct { // Namespaced is true if the resource should be namespaced. Namespaced bool + // SSA is true if Server-Side Apply should be enabled for the API. + SSA bool + // Flags that define which parts should be scaffolded DoAPI bool DoController bool @@ -104,6 +107,7 @@ func (opts Options) UpdateResource(res *resource.Resource, c config.Config) { res.API = &resource.API{ CRDVersion: "v1", Namespaced: opts.Namespaced, + SSA: opts.SSA, } } diff --git a/pkg/plugins/golang/v4/api.go b/pkg/plugins/golang/v4/api.go index 1c785df4965..c794477b7c8 100644 --- a/pkg/plugins/golang/v4/api.go +++ b/pkg/plugins/golang/v4/api.go @@ -109,6 +109,9 @@ func (p *createAPISubcommand) BindFlags(fs *pflag.FlagSet) { fs.BoolVar(&p.options.Namespaced, "namespaced", true, "Resource is namespaced by default; use --namespaced=false to create a cluster-scoped resource") + fs.BoolVar(&p.options.SSA, "ssa", false, + "if set, scaffold this API with Server-Side Apply support (adds +genclient and applyconfiguration generation)") + fs.BoolVar(&p.options.DoController, "controller", true, "Prompt whether to generate the controller by default; "+ "use --controller=true or --controller=false to skip the prompt") @@ -177,6 +180,11 @@ func (p *createAPISubcommand) InjectResource(res *resource.Resource) error { return errors.New("'--external-api-module' requires '--external-api-path' to be specified") } + // Validate that --ssa requires --resource=true + if p.options.SSA && !p.options.DoAPI { + return errors.New("'--ssa' can only be used when creating an API resource ('--resource=true')") + } + p.options.UpdateResource(p.resource, p.config) if err := p.resource.Validate(); err != nil { diff --git a/pkg/plugins/golang/v4/scaffolds/api.go b/pkg/plugins/golang/v4/scaffolds/api.go index a65147cb280..2c31b458d43 100644 --- a/pkg/plugins/golang/v4/scaffolds/api.go +++ b/pkg/plugins/golang/v4/scaffolds/api.go @@ -20,12 +20,14 @@ import ( "errors" "fmt" log "log/slog" + "path/filepath" "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" "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" @@ -103,6 +105,18 @@ func (s *apiScaffolder) Scaffold() error { ); err != nil { return fmt.Errorf("error scaffolding APIs: %w", err) } + + // If SSA is enabled and groupversion_info.go already exists, we need to inject the marker + // (the template only runs when creating a new version package) + if s.resource.API != nil && s.resource.API.SSA { + if err := s.updateGroupVersionInfo(); err != nil { + return fmt.Errorf("error adding ac:generate marker: %w", err) + } + // Update Makefile if this is the first SSA API in the project + if s.isFirstSSAAPI() { + s.updateMakefile() + } + } } if doController { @@ -151,3 +165,140 @@ func (s *apiScaffolder) Scaffold() error { return nil } + +// updateGroupVersionInfo adds the applyconfiguration generation marker +// when groupversion_info.go already exists (e.g., adding a second API to an existing version) +func (s *apiScaffolder) updateGroupVersionInfo() error { + var groupVersionPath string + if s.config.IsMultiGroup() && s.resource.Group != "" { + groupVersionPath = filepath.Join("api", s.resource.Group, s.resource.Version, "groupversion_info.go") + } else { + groupVersionPath = filepath.Join("api", s.resource.Version, "groupversion_info.go") + } + + // Check if marker already exists to avoid duplicates when using --force or multiple kinds + hasMarker, err := util.HasFileContentWith(groupVersionPath, "+kubebuilder:ac:generate=true") + if err != nil { + return fmt.Errorf("error checking for existing ac:generate marker: %w", err) + } + if hasMarker { + return nil + } + + // Add the marker after the object:generate marker + marker := `// +kubebuilder:object:generate=true` + insert := ` +// +kubebuilder:ac:generate=true` + if err := util.InsertCode(groupVersionPath, marker, insert); err != nil { + return fmt.Errorf("error adding ac:generate marker: %w", err) + } + + return nil +} + +// Makefile injection constants +const ( + makefileApplyConfigurationMarker = "applyconfiguration" + + // Patterns to match and replace in Makefile + //nolint:lll + makefileOldObjectGenWithBoilerplateAndYear = "\"$(CONTROLLER_GEN)\" object:headerFile=\"hack/boilerplate.go.txt\",year=$(YEAR) " + + "paths=\"./...\"" + //nolint:lll + makefileNewObjectGenWithBoilerplateAndYear = "\"$(CONTROLLER_GEN)\" object:headerFile=\"hack/boilerplate.go.txt\",year=$(YEAR) " + + "applyconfiguration:headerFile=\"hack/boilerplate.go.txt\" paths=\"./...\"" + + makefileOldObjectGenWithBoilerplate = "\"$(CONTROLLER_GEN)\" object:headerFile=\"hack/boilerplate.go.txt\" " + + "paths=\"./...\"" + makefileNewObjectGenWithBoilerplate = "\"$(CONTROLLER_GEN)\" object:headerFile=\"hack/boilerplate.go.txt\" " + + "applyconfiguration:headerFile=\"hack/boilerplate.go.txt\" paths=\"./...\"" + + makefileOldObjectGenNoBoilerplate = "\"$(CONTROLLER_GEN)\" object paths=\"./...\"" + makefileNewObjectGenNoBoilerplate = "\"$(CONTROLLER_GEN)\" object applyconfiguration paths=\"./...\"" +) + +// isFirstSSAAPI checks if this is the first API with SSA enabled in the project. +// Returns true if there are no other resources with SSA enabled. +func (s *apiScaffolder) isFirstSSAAPI() bool { + // Get all resources in the project + resources, err := s.config.GetResources() + if err != nil { + // If we can't get resources, assume this is the first + return true + } + + // Count resources with SSA enabled (excluding the current one) + for _, res := range resources { + // Skip the current resource + if res.GVK == s.resource.GVK { + continue + } + // Check if this resource has SSA enabled + if res.API != nil && res.API.SSA { + return false + } + } + return true +} + +// updateMakefile modifies the existing controller-gen object generator line to also run +// applyconfiguration generation. Only runs when the first SSA API is created. +// On failure, logs a warning and does not stop scaffolding. +func (s *apiScaffolder) updateMakefile() { + makefilePath := "Makefile" + + // Skip if already updated + hasMarker, err := util.HasFileContentWith(makefilePath, makefileApplyConfigurationMarker) + if err != nil { + log.Warn("unable to read Makefile to add ApplyConfiguration generation for Server-Side Apply. "+ + "Ensure your Makefile 'generate' target includes 'applyconfiguration' in the controller-gen command, "+ + "e.g., '$(CONTROLLER_GEN) object applyconfiguration paths=\"./...\"'", + "path", makefilePath, "error", err) + return + } + if hasMarker { + return + } + + // Try known Makefile formats in order + if err := replaceObjectGenInMakefile(makefilePath); err != nil { + log.Warn("unable to update Makefile 'generate' target to add ApplyConfiguration generation for Server-Side Apply. "+ + "Ensure your Makefile is updated to include 'applyconfiguration' in the controller-gen command. "+ + "For example, change '$(CONTROLLER_GEN) object paths=\"./...\"' to "+ + "'$(CONTROLLER_GEN) object applyconfiguration paths=\"./...\"'", + "error", err) + return + } + + log.Info("applyconfiguration generation added to Makefile generate target") +} + +// replaceObjectGenInMakefile tries known controller-gen object generator patterns in order. +// Returns nil on first successful match, error if none match. +func replaceObjectGenInMakefile(makefilePath string) error { + replacements := []struct { + old string + new string + }{ + { + old: makefileOldObjectGenWithBoilerplateAndYear, + new: makefileNewObjectGenWithBoilerplateAndYear, + }, + { + old: makefileOldObjectGenWithBoilerplate, + new: makefileNewObjectGenWithBoilerplate, + }, + { + old: makefileOldObjectGenNoBoilerplate, + new: makefileNewObjectGenNoBoilerplate, + }, + } + + for _, replacement := range replacements { + if err := util.ReplaceInFile(makefilePath, replacement.old, replacement.new); err == nil { + return nil + } + } + + return fmt.Errorf("none of the known controller-gen object generator patterns matched") +} diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go index f8b9733fbac..df0eabafb21 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/group.go @@ -55,6 +55,9 @@ const groupTemplate = `{{ .Boilerplate }} // Package {{ .Resource.Version }} contains API Schema definitions for the {{ .Resource.Group }} {{ .Resource.Version }} API group. // +kubebuilder:object:generate=true +{{- if .Resource.API.SSA }} +// +kubebuilder:ac:generate=true +{{- end }} // +groupName={{ .Resource.QualifiedGroup }} package {{ .Resource.Version }} diff --git a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go index 2d4a7df7c73..390082187b3 100644 --- a/pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go +++ b/pkg/plugins/golang/v4/scaffolds/internal/templates/api/types.go @@ -109,6 +109,12 @@ type {{ .Resource.Kind }}Status struct { Conditions []metav1.Condition ` + "`" + `json:"conditions,omitempty"` + "`" + ` } +{{- if .Resource.API.SSA }} +// +genclient +{{- if not .Resource.API.Namespaced }} +// +genclient:nonNamespaced +{{- end }} +{{- end }} // +kubebuilder:object:root=true // +kubebuilder:subresource:status {{- if and (not .Resource.API.Namespaced) (not .Resource.IsRegularPlural) }} diff --git a/test/e2e/all/plugin_v4_test.go b/test/e2e/all/plugin_v4_test.go index 9e61eba01c0..95bc7b47fe1 100644 --- a/test/e2e/all/plugin_v4_test.go +++ b/test/e2e/all/plugin_v4_test.go @@ -132,5 +132,15 @@ var _ = Describe("kubebuilder", func() { InstallMethod: helpers.InstallMethodKustomize, }) }) + + It("should generate a runnable project with Server-Side Apply (--ssa)", func() { + helpers.GenerateV4WithSSA(kbc) + helpers.Run(kbc, helpers.RunOptions{ + HasWebhook: false, + HasMetrics: true, + HasNetworkPolicies: false, + InstallMethod: helpers.InstallMethodKustomize, + }) + }) }) }) diff --git a/test/e2e/internal/helpers/generate_v4.go b/test/e2e/internal/helpers/generate_v4.go index 818d91413e3..6cf99d0fcc9 100644 --- a/test/e2e/internal/helpers/generate_v4.go +++ b/test/e2e/internal/helpers/generate_v4.go @@ -240,6 +240,93 @@ func GenerateV4WithCustomWebhookPath(kbc *utils.TestContext) { Expect(err.Error()).To(ContainSubstring("--validation-path can only be used with --programmatic-validation")) } +// GenerateV4WithSSA implements a go/v4 plugin project with Server-Side Apply enabled. +func GenerateV4WithSSA(kbc *utils.TestContext) { + initingTheProject(kbc) + + By("creating API with Server-Side Apply (--ssa)") + err := kbc.CreateAPI( + "--group", kbc.Group, + "--version", kbc.Version, + "--kind", kbc.Kind, + "--namespaced", + "--resource", + "--controller", + "--ssa", + "--make=false", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to create API with SSA") + + 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()) + + 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()) +} + +// GenerateV4WithSSAClusterScoped implements a go/v4 plugin project with cluster-scoped Server-Side Apply enabled. +func GenerateV4WithSSAClusterScoped(kbc *utils.TestContext) { + initingTheProject(kbc) + + By("creating cluster-scoped API with Server-Side Apply (--ssa --namespaced=false)") + err := kbc.CreateAPI( + "--group", kbc.Group, + "--version", kbc.Version, + "--kind", kbc.Kind, + "--namespaced=false", + "--resource", + "--controller", + "--ssa", + "--make=false", + ) + Expect(err).NotTo(HaveOccurred(), "Failed to create cluster-scoped API with SSA") + + 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()) + + By("verifying +genclient:nonNamespaced marker is present") + typesFilePath := filepath.Join(kbc.Dir, "api", kbc.Version, fmt.Sprintf("%s_types.go", strings.ToLower(kbc.Kind))) + content, err := os.ReadFile(typesFilePath) + Expect(err).NotTo(HaveOccurred(), "Failed to read types file") + Expect(string(content)).To(ContainSubstring("+genclient:nonNamespaced"), + "Types file should contain +genclient:nonNamespaced marker for cluster-scoped SSA") + + 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()) +} + func creatingAPI(kbc *utils.TestContext) { By("creating API definition") err := kbc.CreateAPI( diff --git a/test/testdata/generate.sh b/test/testdata/generate.sh index f6fe387c456..654b2037324 100755 --- a/test/testdata/generate.sh +++ b/test/testdata/generate.sh @@ -68,6 +68,9 @@ function scaffold_test_project { # 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 + + # Creating API with Server-Side Apply (SSA) - use same group as other APIs + $kb create api --group crew --version v1 --kind Navigator --controller=true --resource=true --ssa --make=false fi if [[ $project =~ multigroup ]]; then @@ -101,6 +104,9 @@ function scaffold_test_project { # 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 + + # Creating API with Server-Side Apply (SSA) + $kb create api --group sea-creatures --version v1 --kind Prawn --controller=true --resource=true --ssa --make=false fi if [[ $project =~ with-plugins ]] ; then diff --git a/testdata/project-v4-multigroup/Makefile b/testdata/project-v4-multigroup/Makefile index 2208e6449f5..0865be2348d 100644 --- a/testdata/project-v4-multigroup/Makefile +++ b/testdata/project-v4-multigroup/Makefile @@ -49,7 +49,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..." + "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. diff --git a/testdata/project-v4-multigroup/PROJECT b/testdata/project-v4-multigroup/PROJECT index 989096fa976..3a70152c5c9 100644 --- a/testdata/project-v4-multigroup/PROJECT +++ b/testdata/project-v4-multigroup/PROJECT @@ -156,6 +156,16 @@ resources: webhooks: validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + ssa: true + controller: true + domain: testproject.org + group: sea-creatures + kind: Prawn + path: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1 + version: v1 - api: crdVersion: v1 namespaced: true diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/internal/internal.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/internal/internal.go new file mode 100644 index 00000000000..ed7dc0ff1e0 --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/internal/internal.go @@ -0,0 +1,61 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 internal + +import ( + fmt "fmt" + sync "sync" + + typed "sigs.k8s.io/structured-merge-diff/v6/typed" +) + +func Parser() *typed.Parser { + parserOnce.Do(func() { + var err error + parser, err = typed.NewParser(schemaYAML) + if err != nil { + panic(fmt.Sprintf("Failed to parse schema: %v", err)) + } + }) + return parser +} + +var parserOnce sync.Once +var parser *typed.Parser +var schemaYAML = typed.YAMLObject(`types: +- name: __untyped_atomic_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic +- name: __untyped_deduced_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable +`) diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawn.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawn.go new file mode 100644 index 00000000000..4618c056792 --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawn.go @@ -0,0 +1,247 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 ( + apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + metav1 "k8s.io/client-go/applyconfigurations/meta/v1" +) + +// PrawnApplyConfiguration represents a declarative configuration of the Prawn type for use +// with apply. +// +// Prawn is the Schema for the prawns API +type PrawnApplyConfiguration struct { + metav1.TypeMetaApplyConfiguration `json:",inline"` + // metadata is a standard object metadata + *metav1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // spec defines the desired state of Prawn + Spec *PrawnSpecApplyConfiguration `json:"spec,omitempty"` + // status defines the observed state of Prawn + Status *PrawnStatusApplyConfiguration `json:"status,omitempty"` +} + +// Prawn constructs a declarative configuration of the Prawn type for use with +// apply. +func Prawn(name, namespace string) *PrawnApplyConfiguration { + b := &PrawnApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Prawn") + b.WithAPIVersion("sea-creatures.testproject.org/v1") + return b +} + +func (b PrawnApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithKind(value string) *PrawnApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithAPIVersion(value string) *PrawnApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithName(value string) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithGenerateName(value string) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithNamespace(value string) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithUID(value types.UID) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithResourceVersion(value string) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithGeneration(value int64) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithCreationTimestamp(value apismetav1.Time) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithDeletionTimestamp(value apismetav1.Time) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *PrawnApplyConfiguration) WithLabels(entries map[string]string) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *PrawnApplyConfiguration) WithAnnotations(entries map[string]string) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *PrawnApplyConfiguration) WithOwnerReferences(values ...*metav1.OwnerReferenceApplyConfiguration) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *PrawnApplyConfiguration) WithFinalizers(values ...string) *PrawnApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *PrawnApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &metav1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithSpec(value *PrawnSpecApplyConfiguration) *PrawnApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *PrawnApplyConfiguration) WithStatus(value *PrawnStatusApplyConfiguration) *PrawnApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *PrawnApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *PrawnApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *PrawnApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *PrawnApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnspec.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnspec.go new file mode 100644 index 00000000000..3fa77856266 --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnspec.go @@ -0,0 +1,43 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 + +// PrawnSpecApplyConfiguration represents a declarative configuration of the PrawnSpec type for use +// with apply. +// +// 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. +// PrawnSpec defines the desired state of Prawn +type PrawnSpecApplyConfiguration struct { + // foo is an example field of Prawn. Edit prawn_types.go to remove/update + Foo *string `json:"foo,omitempty"` +} + +// PrawnSpecApplyConfiguration constructs a declarative configuration of the PrawnSpec type for use with +// apply. +func PrawnSpec() *PrawnSpecApplyConfiguration { + return &PrawnSpecApplyConfiguration{} +} + +// WithFoo sets the Foo field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Foo field is set to the value of the last call. +func (b *PrawnSpecApplyConfiguration) WithFoo(value string) *PrawnSpecApplyConfiguration { + b.Foo = &value + return b +} diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnstatus.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnstatus.go new file mode 100644 index 00000000000..f50c9867465 --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnstatus.go @@ -0,0 +1,58 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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/client-go/applyconfigurations/meta/v1" +) + +// PrawnStatusApplyConfiguration represents a declarative configuration of the PrawnStatus type for use +// with apply. +// +// PrawnStatus defines the observed state of Prawn. +type PrawnStatusApplyConfiguration struct { + // conditions represent the current state of the Prawn 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. + Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// PrawnStatusApplyConfiguration constructs a declarative configuration of the PrawnStatus type for use with +// apply. +func PrawnStatus() *PrawnStatusApplyConfiguration { + return &PrawnStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *PrawnStatusApplyConfiguration) WithConditions(values ...*metav1.ConditionApplyConfiguration) *PrawnStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/utils.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/utils.go new file mode 100644 index 00000000000..e42efe26e6e --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/utils.go @@ -0,0 +1,47 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 applyconfiguration + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + v1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1" + internal "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/internal" + seacreaturesv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1" +) + +// ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no +// apply configuration type exists for the given GroupVersionKind. +func ForKind(kind schema.GroupVersionKind) interface{} { + switch kind { + // Group=sea-creatures.testproject.org, Version=v1 + case v1.SchemeGroupVersion.WithKind("Prawn"): + return &seacreaturesv1.PrawnApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("PrawnSpec"): + return &seacreaturesv1.PrawnSpecApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("PrawnStatus"): + return &seacreaturesv1.PrawnStatusApplyConfiguration{} + + } + return nil +} + +func NewTypeConverter(scheme *runtime.Scheme) managedfields.TypeConverter { + return managedfields.NewSchemeTypeConverter(scheme, internal.Parser()) +} diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/groupversion_info.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/groupversion_info.go new file mode 100644 index 00000000000..f1cfb4b66e0 --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/groupversion_info.go @@ -0,0 +1,45 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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 sea-creatures v1 API group. +// +kubebuilder:object:generate=true +// +kubebuilder:ac:generate=true +// +groupName=sea-creatures.testproject.org +package v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +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: "v1"} + + // GroupVersion is an alias for SchemeGroupVersion, for backward compatibility. + GroupVersion = SchemeGroupVersion + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error { + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + return nil + }) + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/prawn_types.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/prawn_types.go new file mode 100644 index 00000000000..b402e2995f2 --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/prawn_types.go @@ -0,0 +1,97 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "k8s.io/apimachinery/pkg/runtime" +) + +// 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. + +// PrawnSpec defines the desired state of Prawn +type PrawnSpec 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 Prawn. Edit prawn_types.go to remove/update + // +optional + Foo *string `json:"foo,omitempty"` +} + +// PrawnStatus defines the observed state of Prawn. +type PrawnStatus 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 Prawn 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"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Prawn is the Schema for the prawns API +type Prawn struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // spec defines the desired state of Prawn + // +required + Spec PrawnSpec `json:"spec"` + + // status defines the observed state of Prawn + // +optional + Status PrawnStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// PrawnList contains a list of Prawn +type PrawnList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []Prawn `json:"items"` +} + +func init() { + SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(SchemeGroupVersion, &Prawn{}, &PrawnList{}) + return nil + }) +} diff --git a/testdata/project-v4-multigroup/api/sea-creatures/v1/zz_generated.deepcopy.go b/testdata/project-v4-multigroup/api/sea-creatures/v1/zz_generated.deepcopy.go new file mode 100644 index 00000000000..5f6fbe1939d --- /dev/null +++ b/testdata/project-v4-multigroup/api/sea-creatures/v1/zz_generated.deepcopy.go @@ -0,0 +1,127 @@ +//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" + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Prawn) DeepCopyInto(out *Prawn) { + *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 Prawn. +func (in *Prawn) DeepCopy() *Prawn { + if in == nil { + return nil + } + out := new(Prawn) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Prawn) 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 *PrawnList) DeepCopyInto(out *PrawnList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Prawn, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PrawnList. +func (in *PrawnList) DeepCopy() *PrawnList { + if in == nil { + return nil + } + out := new(PrawnList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PrawnList) 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 *PrawnSpec) DeepCopyInto(out *PrawnSpec) { + *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 PrawnSpec. +func (in *PrawnSpec) DeepCopy() *PrawnSpec { + if in == nil { + return nil + } + out := new(PrawnSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PrawnStatus) DeepCopyInto(out *PrawnStatus) { + *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 PrawnStatus. +func (in *PrawnStatus) DeepCopy() *PrawnStatus { + if in == nil { + return nil + } + out := new(PrawnStatus) + in.DeepCopyInto(out) + return out +} diff --git a/testdata/project-v4-multigroup/cmd/main.go b/testdata/project-v4-multigroup/cmd/main.go index 62a18210464..c735ba2232c 100644 --- a/testdata/project-v4-multigroup/cmd/main.go +++ b/testdata/project-v4-multigroup/cmd/main.go @@ -44,6 +44,7 @@ import ( 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" + seacreaturesv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/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" @@ -87,6 +88,7 @@ func init() { utilruntime.Must(foov1.AddToScheme(scheme)) utilruntime.Must(fizv1.AddToScheme(scheme)) utilruntime.Must(certmanagerv1.AddToScheme(scheme)) + utilruntime.Must(seacreaturesv1.AddToScheme(scheme)) utilruntime.Must(examplecomv1alpha1.AddToScheme(scheme)) utilruntime.Must(examplecomv1.AddToScheme(scheme)) utilruntime.Must(examplecomv2.AddToScheme(scheme)) @@ -345,6 +347,13 @@ func main() { os.Exit(1) } } + if err := (&seacreaturescontroller.PrawnReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Failed to create controller", "controller", "sea-creatures-prawn") + os.Exit(1) + } if err := (&examplecomcontroller.MemcachedReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), diff --git a/testdata/project-v4-multigroup/config/crd/bases/sea-creatures.testproject.org_prawns.yaml b/testdata/project-v4-multigroup/config/crd/bases/sea-creatures.testproject.org_prawns.yaml new file mode 100644 index 00000000000..51e7ce9a7b4 --- /dev/null +++ b/testdata/project-v4-multigroup/config/crd/bases/sea-creatures.testproject.org_prawns.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: prawns.sea-creatures.testproject.org +spec: + group: sea-creatures.testproject.org + names: + kind: Prawn + listKind: PrawnList + plural: prawns + singular: prawn + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Prawn is the Schema for the prawns 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 Prawn + properties: + foo: + description: foo is an example field of Prawn. Edit prawn_types.go + to remove/update + type: string + type: object + status: + description: status defines the observed state of Prawn + properties: + conditions: + description: |- + conditions represent the current state of the Prawn 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: {} diff --git a/testdata/project-v4-multigroup/config/crd/kustomization.yaml b/testdata/project-v4-multigroup/config/crd/kustomization.yaml index 70c560eecae..188887975e7 100644 --- a/testdata/project-v4-multigroup/config/crd/kustomization.yaml +++ b/testdata/project-v4-multigroup/config/crd/kustomization.yaml @@ -11,6 +11,7 @@ resources: - bases/foo.policy.testproject.org_healthcheckpolicies.yaml - bases/foo.testproject.org_bars.yaml - bases/fiz.testproject.org_bars.yaml +- bases/sea-creatures.testproject.org_prawns.yaml - bases/example.com.testproject.org_memcacheds.yaml - bases/example.com.testproject.org_busyboxes.yaml - bases/example.com.testproject.org_wordpresses.yaml diff --git a/testdata/project-v4-multigroup/config/rbac/kustomization.yaml b/testdata/project-v4-multigroup/config/rbac/kustomization.yaml index b5059bfb5c6..b820b680ff9 100644 --- a/testdata/project-v4-multigroup/config/rbac/kustomization.yaml +++ b/testdata/project-v4-multigroup/config/rbac/kustomization.yaml @@ -31,6 +31,9 @@ resources: - example.com_memcached_admin_role.yaml - example.com_memcached_editor_role.yaml - example.com_memcached_viewer_role.yaml +- sea-creatures_prawn_admin_role.yaml +- sea-creatures_prawn_editor_role.yaml +- sea-creatures_prawn_viewer_role.yaml - fiz_bar_admin_role.yaml - fiz_bar_editor_role.yaml - fiz_bar_viewer_role.yaml diff --git a/testdata/project-v4-multigroup/config/rbac/role.yaml b/testdata/project-v4-multigroup/config/rbac/role.yaml index 465c17614d4..b5e026cf937 100644 --- a/testdata/project-v4-multigroup/config/rbac/role.yaml +++ b/testdata/project-v4-multigroup/config/rbac/role.yaml @@ -189,6 +189,7 @@ rules: resources: - krakens - leviathans + - prawns verbs: - create - delete @@ -202,6 +203,7 @@ rules: resources: - krakens/finalizers - leviathans/finalizers + - prawns/finalizers verbs: - update - apiGroups: @@ -209,6 +211,7 @@ rules: resources: - krakens/status - leviathans/status + - prawns/status verbs: - get - patch diff --git a/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_admin_role.yaml b/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_admin_role.yaml new file mode 100644 index 00000000000..c1b8708dbd1 --- /dev/null +++ b/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_admin_role.yaml @@ -0,0 +1,27 @@ +# 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-prawn-admin-role +rules: +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns + verbs: + - '*' +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns/status + verbs: + - get diff --git a/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_editor_role.yaml b/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_editor_role.yaml new file mode 100644 index 00000000000..55b4cd7b5f5 --- /dev/null +++ b/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_editor_role.yaml @@ -0,0 +1,33 @@ +# 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-prawn-editor-role +rules: +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns/status + verbs: + - get diff --git a/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_viewer_role.yaml b/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_viewer_role.yaml new file mode 100644 index 00000000000..be91ed9838d --- /dev/null +++ b/testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_viewer_role.yaml @@ -0,0 +1,29 @@ +# 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-prawn-viewer-role +rules: +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns + verbs: + - get + - list + - watch +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns/status + verbs: + - get diff --git a/testdata/project-v4-multigroup/config/samples/kustomization.yaml b/testdata/project-v4-multigroup/config/samples/kustomization.yaml index 703c10a4a61..76a911e908d 100644 --- a/testdata/project-v4-multigroup/config/samples/kustomization.yaml +++ b/testdata/project-v4-multigroup/config/samples/kustomization.yaml @@ -9,6 +9,7 @@ resources: - foo.policy_v1_healthcheckpolicy.yaml - foo_v1_bar.yaml - fiz_v1_bar.yaml +- sea-creatures_v1_prawn.yaml - example.com_v1alpha1_memcached.yaml - example.com_v1alpha1_busybox.yaml - example.com_v1_wordpress.yaml diff --git a/testdata/project-v4-multigroup/config/samples/sea-creatures_v1_prawn.yaml b/testdata/project-v4-multigroup/config/samples/sea-creatures_v1_prawn.yaml new file mode 100644 index 00000000000..d1c1fdf3752 --- /dev/null +++ b/testdata/project-v4-multigroup/config/samples/sea-creatures_v1_prawn.yaml @@ -0,0 +1,9 @@ +apiVersion: sea-creatures.testproject.org/v1 +kind: Prawn +metadata: + labels: + app.kubernetes.io/name: project-v4-multigroup + app.kubernetes.io/managed-by: kustomize + name: prawn-sample +spec: + # TODO(user): Add fields here diff --git a/testdata/project-v4-multigroup/dist/install.yaml b/testdata/project-v4-multigroup/dist/install.yaml index 0f68a1fd57b..d88982d679a 100644 --- a/testdata/project-v4-multigroup/dist/install.yaml +++ b/testdata/project-v4-multigroup/dist/install.yaml @@ -1406,6 +1406,132 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: prawns.sea-creatures.testproject.org +spec: + group: sea-creatures.testproject.org + names: + kind: Prawn + listKind: PrawnList + plural: prawns + singular: prawn + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Prawn is the Schema for the prawns 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 Prawn + properties: + foo: + description: foo is an example field of Prawn. Edit prawn_types.go + to remove/update + type: string + type: object + status: + description: status defines the observed state of Prawn + properties: + conditions: + description: |- + conditions represent the current state of the Prawn 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 @@ -2388,6 +2514,7 @@ rules: resources: - krakens - leviathans + - prawns verbs: - create - delete @@ -2401,6 +2528,7 @@ rules: resources: - krakens/finalizers - leviathans/finalizers + - prawns/finalizers verbs: - update - apiGroups: @@ -2408,6 +2536,7 @@ rules: resources: - krakens/status - leviathans/status + - prawns/status verbs: - get - patch @@ -2617,6 +2746,77 @@ rules: --- 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-prawn-admin-role +rules: +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns + verbs: + - '*' +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns/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-prawn-editor-role +rules: +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns/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-prawn-viewer-role +rules: +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns + verbs: + - get + - list + - watch +- apiGroups: + - sea-creatures.testproject.org + resources: + - prawns/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize diff --git a/testdata/project-v4-multigroup/go.mod b/testdata/project-v4-multigroup/go.mod index 448412eb7ac..1f528c7229e 100644 --- a/testdata/project-v4-multigroup/go.mod +++ b/testdata/project-v4-multigroup/go.mod @@ -11,6 +11,7 @@ require ( k8s.io/client-go v0.36.0 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 sigs.k8s.io/controller-runtime v0.24.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 ) require ( @@ -98,6 +99,5 @@ require ( 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 ) diff --git a/testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller.go b/testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller.go new file mode 100644 index 00000000000..6d6e3204052 --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller.go @@ -0,0 +1,63 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + + seacreaturesv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1" +) + +// PrawnReconciler reconciles a Prawn object +type PrawnReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=prawns,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=prawns/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=sea-creatures.testproject.org,resources=prawns/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 Prawn 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.24.0/pkg/reconcile +func (r *PrawnReconciler) 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 *PrawnReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&seacreaturesv1.Prawn{}). + Named("sea-creatures-prawn"). + Complete(r) +} diff --git a/testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller_test.go b/testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller_test.go new file mode 100644 index 00000000000..391c6d5a43e --- /dev/null +++ b/testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + + seacreaturesv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/v1" +) + +var _ = Describe("Prawn 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 + } + prawn := &seacreaturesv1.Prawn{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Prawn") + err := k8sClient.Get(ctx, typeNamespacedName, prawn) + if err != nil && errors.IsNotFound(err) { + resource := &seacreaturesv1.Prawn{ + 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 := &seacreaturesv1.Prawn{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Prawn") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &PrawnReconciler{ + 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. + }) + }) +}) diff --git a/testdata/project-v4-multigroup/internal/controller/sea-creatures/suite_test.go b/testdata/project-v4-multigroup/internal/controller/sea-creatures/suite_test.go index 3068c57405a..f3b7877e9c5 100644 --- a/testdata/project-v4-multigroup/internal/controller/sea-creatures/suite_test.go +++ b/testdata/project-v4-multigroup/internal/controller/sea-creatures/suite_test.go @@ -33,6 +33,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" + seacreaturesv1 "sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup/api/sea-creatures/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" // +kubebuilder:scaffold:imports @@ -67,6 +68,9 @@ var _ = BeforeSuite(func() { err = seacreaturesv1beta2.AddToScheme(scheme.Scheme) Expect(err).NotTo(HaveOccurred()) + err = seacreaturesv1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme By("bootstrapping test environment") diff --git a/testdata/project-v4/Makefile b/testdata/project-v4/Makefile index 1383b541bd4..020b4d63b6a 100644 --- a/testdata/project-v4/Makefile +++ b/testdata/project-v4/Makefile @@ -49,7 +49,7 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust .PHONY: generate generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..." + "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="./..." .PHONY: fmt fmt: ## Run go fmt against code. diff --git a/testdata/project-v4/PROJECT b/testdata/project-v4/PROJECT index 433ac2a4671..1444990c1f5 100644 --- a/testdata/project-v4/PROJECT +++ b/testdata/project-v4/PROJECT @@ -110,4 +110,14 @@ resources: defaulting: true validation: true webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + ssa: true + controller: true + domain: testproject.org + group: crew + kind: Navigator + path: sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1 + version: v1 version: "3" diff --git a/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiral.go b/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiral.go new file mode 100644 index 00000000000..d6ca323f00d --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiral.go @@ -0,0 +1,289 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 ( + apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + metav1 "k8s.io/client-go/applyconfigurations/meta/v1" + apiv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" + internal "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1/applyconfiguration/internal" +) + +// AdmiralApplyConfiguration represents a declarative configuration of the Admiral type for use +// with apply. +// +// Admiral is the Schema for the admirales API +type AdmiralApplyConfiguration struct { + metav1.TypeMetaApplyConfiguration `json:",inline"` + // metadata is a standard object metadata + *metav1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // spec defines the desired state of Admiral + Spec *AdmiralSpecApplyConfiguration `json:"spec,omitempty"` + // status defines the observed state of Admiral + Status *AdmiralStatusApplyConfiguration `json:"status,omitempty"` +} + +// Admiral constructs a declarative configuration of the Admiral type for use with +// apply. +func Admiral(name string) *AdmiralApplyConfiguration { + b := &AdmiralApplyConfiguration{} + b.WithName(name) + b.WithKind("Admiral") + b.WithAPIVersion("crew.testproject.org/v1") + return b +} + +// ExtractAdmiralFrom extracts the applied configuration owned by fieldManager from +// admiral for the specified subresource. Pass an empty string for subresource to extract +// the main resource. Common subresources include "status", "scale", etc. +// admiral must be a unmodified Admiral API object that was retrieved from the Kubernetes API. +// ExtractAdmiralFrom provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractAdmiralFrom(admiral *apiv1.Admiral, fieldManager string, subresource string) (*AdmiralApplyConfiguration, error) { + b := &AdmiralApplyConfiguration{} + err := managedfields.ExtractInto(admiral, internal.Parser().Type("io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.Admiral"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(admiral.Name) + + b.WithKind("Admiral") + b.WithAPIVersion("crew.testproject.org/v1") + return b, nil +} + +// ExtractAdmiral extracts the applied configuration owned by fieldManager from +// admiral. If no managedFields are found in admiral for fieldManager, a +// AdmiralApplyConfiguration is returned with only the Name, Namespace (if applicable), +// APIVersion and Kind populated. It is possible that no managed fields were found for because other +// field managers have taken ownership of all the fields previously owned by fieldManager, or because +// the fieldManager never owned fields any fields. +// admiral must be a unmodified Admiral API object that was retrieved from the Kubernetes API. +// ExtractAdmiral provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractAdmiral(admiral *apiv1.Admiral, fieldManager string) (*AdmiralApplyConfiguration, error) { + return ExtractAdmiralFrom(admiral, fieldManager, "") +} + +// ExtractAdmiralStatus extracts the applied configuration owned by fieldManager from +// admiral for the status subresource. +func ExtractAdmiralStatus(admiral *apiv1.Admiral, fieldManager string) (*AdmiralApplyConfiguration, error) { + return ExtractAdmiralFrom(admiral, fieldManager, "status") +} + +func (b AdmiralApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithKind(value string) *AdmiralApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithAPIVersion(value string) *AdmiralApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithName(value string) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithGenerateName(value string) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithNamespace(value string) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithUID(value types.UID) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithResourceVersion(value string) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithGeneration(value int64) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithCreationTimestamp(value apismetav1.Time) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithDeletionTimestamp(value apismetav1.Time) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *AdmiralApplyConfiguration) WithLabels(entries map[string]string) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *AdmiralApplyConfiguration) WithAnnotations(entries map[string]string) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *AdmiralApplyConfiguration) WithOwnerReferences(values ...*metav1.OwnerReferenceApplyConfiguration) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *AdmiralApplyConfiguration) WithFinalizers(values ...string) *AdmiralApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *AdmiralApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &metav1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithSpec(value *AdmiralSpecApplyConfiguration) *AdmiralApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *AdmiralApplyConfiguration) WithStatus(value *AdmiralStatusApplyConfiguration) *AdmiralApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *AdmiralApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *AdmiralApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *AdmiralApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *AdmiralApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralspec.go b/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralspec.go new file mode 100644 index 00000000000..103951076ba --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralspec.go @@ -0,0 +1,43 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 + +// AdmiralSpecApplyConfiguration represents a declarative configuration of the AdmiralSpec type for use +// with apply. +// +// 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 AdmiralSpecApplyConfiguration struct { + // foo is an example field of Admiral. Edit admiral_types.go to remove/update + Foo *string `json:"foo,omitempty"` +} + +// AdmiralSpecApplyConfiguration constructs a declarative configuration of the AdmiralSpec type for use with +// apply. +func AdmiralSpec() *AdmiralSpecApplyConfiguration { + return &AdmiralSpecApplyConfiguration{} +} + +// WithFoo sets the Foo field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Foo field is set to the value of the last call. +func (b *AdmiralSpecApplyConfiguration) WithFoo(value string) *AdmiralSpecApplyConfiguration { + b.Foo = &value + return b +} diff --git a/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralstatus.go b/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralstatus.go new file mode 100644 index 00000000000..5efb6c13f49 --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralstatus.go @@ -0,0 +1,58 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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/client-go/applyconfigurations/meta/v1" +) + +// AdmiralStatusApplyConfiguration represents a declarative configuration of the AdmiralStatus type for use +// with apply. +// +// AdmiralStatus defines the observed state of Admiral. +type AdmiralStatusApplyConfiguration struct { + // 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. + Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// AdmiralStatusApplyConfiguration constructs a declarative configuration of the AdmiralStatus type for use with +// apply. +func AdmiralStatus() *AdmiralStatusApplyConfiguration { + return &AdmiralStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *AdmiralStatusApplyConfiguration) WithConditions(values ...*metav1.ConditionApplyConfiguration) *AdmiralStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigator.go b/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigator.go new file mode 100644 index 00000000000..c327a470fd9 --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigator.go @@ -0,0 +1,291 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 ( + apismetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + metav1 "k8s.io/client-go/applyconfigurations/meta/v1" + apiv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" + internal "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1/applyconfiguration/internal" +) + +// NavigatorApplyConfiguration represents a declarative configuration of the Navigator type for use +// with apply. +// +// Navigator is the Schema for the navigators API +type NavigatorApplyConfiguration struct { + metav1.TypeMetaApplyConfiguration `json:",inline"` + // metadata is a standard object metadata + *metav1.ObjectMetaApplyConfiguration `json:"metadata,omitempty"` + // spec defines the desired state of Navigator + Spec *NavigatorSpecApplyConfiguration `json:"spec,omitempty"` + // status defines the observed state of Navigator + Status *NavigatorStatusApplyConfiguration `json:"status,omitempty"` +} + +// Navigator constructs a declarative configuration of the Navigator type for use with +// apply. +func Navigator(name, namespace string) *NavigatorApplyConfiguration { + b := &NavigatorApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Navigator") + b.WithAPIVersion("crew.testproject.org/v1") + return b +} + +// ExtractNavigatorFrom extracts the applied configuration owned by fieldManager from +// navigator for the specified subresource. Pass an empty string for subresource to extract +// the main resource. Common subresources include "status", "scale", etc. +// navigator must be a unmodified Navigator API object that was retrieved from the Kubernetes API. +// ExtractNavigatorFrom provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractNavigatorFrom(navigator *apiv1.Navigator, fieldManager string, subresource string) (*NavigatorApplyConfiguration, error) { + b := &NavigatorApplyConfiguration{} + err := managedfields.ExtractInto(navigator, internal.Parser().Type("io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.Navigator"), fieldManager, b, subresource) + if err != nil { + return nil, err + } + b.WithName(navigator.Name) + b.WithNamespace(navigator.Namespace) + + b.WithKind("Navigator") + b.WithAPIVersion("crew.testproject.org/v1") + return b, nil +} + +// ExtractNavigator extracts the applied configuration owned by fieldManager from +// navigator. If no managedFields are found in navigator for fieldManager, a +// NavigatorApplyConfiguration is returned with only the Name, Namespace (if applicable), +// APIVersion and Kind populated. It is possible that no managed fields were found for because other +// field managers have taken ownership of all the fields previously owned by fieldManager, or because +// the fieldManager never owned fields any fields. +// navigator must be a unmodified Navigator API object that was retrieved from the Kubernetes API. +// ExtractNavigator provides a way to perform a extract/modify-in-place/apply workflow. +// Note that an extracted apply configuration will contain fewer fields than what the fieldManager previously +// applied if another fieldManager has updated or force applied any of the previously applied fields. +func ExtractNavigator(navigator *apiv1.Navigator, fieldManager string) (*NavigatorApplyConfiguration, error) { + return ExtractNavigatorFrom(navigator, fieldManager, "") +} + +// ExtractNavigatorStatus extracts the applied configuration owned by fieldManager from +// navigator for the status subresource. +func ExtractNavigatorStatus(navigator *apiv1.Navigator, fieldManager string) (*NavigatorApplyConfiguration, error) { + return ExtractNavigatorFrom(navigator, fieldManager, "status") +} + +func (b NavigatorApplyConfiguration) IsApplyConfiguration() {} + +// WithKind sets the Kind field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Kind field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithKind(value string) *NavigatorApplyConfiguration { + b.TypeMetaApplyConfiguration.Kind = &value + return b +} + +// WithAPIVersion sets the APIVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the APIVersion field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithAPIVersion(value string) *NavigatorApplyConfiguration { + b.TypeMetaApplyConfiguration.APIVersion = &value + return b +} + +// WithName sets the Name field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Name field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithName(value string) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Name = &value + return b +} + +// WithGenerateName sets the GenerateName field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the GenerateName field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithGenerateName(value string) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.GenerateName = &value + return b +} + +// WithNamespace sets the Namespace field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Namespace field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithNamespace(value string) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Namespace = &value + return b +} + +// WithUID sets the UID field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the UID field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithUID(value types.UID) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.UID = &value + return b +} + +// WithResourceVersion sets the ResourceVersion field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the ResourceVersion field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithResourceVersion(value string) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.ResourceVersion = &value + return b +} + +// WithGeneration sets the Generation field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Generation field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithGeneration(value int64) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.Generation = &value + return b +} + +// WithCreationTimestamp sets the CreationTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the CreationTimestamp field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithCreationTimestamp(value apismetav1.Time) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.CreationTimestamp = &value + return b +} + +// WithDeletionTimestamp sets the DeletionTimestamp field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionTimestamp field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithDeletionTimestamp(value apismetav1.Time) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionTimestamp = &value + return b +} + +// WithDeletionGracePeriodSeconds sets the DeletionGracePeriodSeconds field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the DeletionGracePeriodSeconds field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithDeletionGracePeriodSeconds(value int64) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + b.ObjectMetaApplyConfiguration.DeletionGracePeriodSeconds = &value + return b +} + +// WithLabels puts the entries into the Labels field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Labels field, +// overwriting an existing map entries in Labels field with the same key. +func (b *NavigatorApplyConfiguration) WithLabels(entries map[string]string) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Labels == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Labels = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Labels[k] = v + } + return b +} + +// WithAnnotations puts the entries into the Annotations field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, the entries provided by each call will be put on the Annotations field, +// overwriting an existing map entries in Annotations field with the same key. +func (b *NavigatorApplyConfiguration) WithAnnotations(entries map[string]string) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + if b.ObjectMetaApplyConfiguration.Annotations == nil && len(entries) > 0 { + b.ObjectMetaApplyConfiguration.Annotations = make(map[string]string, len(entries)) + } + for k, v := range entries { + b.ObjectMetaApplyConfiguration.Annotations[k] = v + } + return b +} + +// WithOwnerReferences adds the given value to the OwnerReferences field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the OwnerReferences field. +func (b *NavigatorApplyConfiguration) WithOwnerReferences(values ...*metav1.OwnerReferenceApplyConfiguration) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + if values[i] == nil { + panic("nil value passed to WithOwnerReferences") + } + b.ObjectMetaApplyConfiguration.OwnerReferences = append(b.ObjectMetaApplyConfiguration.OwnerReferences, *values[i]) + } + return b +} + +// WithFinalizers adds the given value to the Finalizers field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Finalizers field. +func (b *NavigatorApplyConfiguration) WithFinalizers(values ...string) *NavigatorApplyConfiguration { + b.ensureObjectMetaApplyConfigurationExists() + for i := range values { + b.ObjectMetaApplyConfiguration.Finalizers = append(b.ObjectMetaApplyConfiguration.Finalizers, values[i]) + } + return b +} + +func (b *NavigatorApplyConfiguration) ensureObjectMetaApplyConfigurationExists() { + if b.ObjectMetaApplyConfiguration == nil { + b.ObjectMetaApplyConfiguration = &metav1.ObjectMetaApplyConfiguration{} + } +} + +// WithSpec sets the Spec field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Spec field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithSpec(value *NavigatorSpecApplyConfiguration) *NavigatorApplyConfiguration { + b.Spec = value + return b +} + +// WithStatus sets the Status field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Status field is set to the value of the last call. +func (b *NavigatorApplyConfiguration) WithStatus(value *NavigatorStatusApplyConfiguration) *NavigatorApplyConfiguration { + b.Status = value + return b +} + +// GetKind retrieves the value of the Kind field in the declarative configuration. +func (b *NavigatorApplyConfiguration) GetKind() *string { + return b.TypeMetaApplyConfiguration.Kind +} + +// GetAPIVersion retrieves the value of the APIVersion field in the declarative configuration. +func (b *NavigatorApplyConfiguration) GetAPIVersion() *string { + return b.TypeMetaApplyConfiguration.APIVersion +} + +// GetName retrieves the value of the Name field in the declarative configuration. +func (b *NavigatorApplyConfiguration) GetName() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Name +} + +// GetNamespace retrieves the value of the Namespace field in the declarative configuration. +func (b *NavigatorApplyConfiguration) GetNamespace() *string { + b.ensureObjectMetaApplyConfigurationExists() + return b.ObjectMetaApplyConfiguration.Namespace +} diff --git a/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorspec.go b/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorspec.go new file mode 100644 index 00000000000..0cc23c1b8b5 --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorspec.go @@ -0,0 +1,43 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 + +// NavigatorSpecApplyConfiguration represents a declarative configuration of the NavigatorSpec type for use +// with apply. +// +// 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. +// NavigatorSpec defines the desired state of Navigator +type NavigatorSpecApplyConfiguration struct { + // foo is an example field of Navigator. Edit navigator_types.go to remove/update + Foo *string `json:"foo,omitempty"` +} + +// NavigatorSpecApplyConfiguration constructs a declarative configuration of the NavigatorSpec type for use with +// apply. +func NavigatorSpec() *NavigatorSpecApplyConfiguration { + return &NavigatorSpecApplyConfiguration{} +} + +// WithFoo sets the Foo field in the declarative configuration to the given value +// and returns the receiver, so that objects can be built by chaining "With" function invocations. +// If called multiple times, the Foo field is set to the value of the last call. +func (b *NavigatorSpecApplyConfiguration) WithFoo(value string) *NavigatorSpecApplyConfiguration { + b.Foo = &value + return b +} diff --git a/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorstatus.go b/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorstatus.go new file mode 100644 index 00000000000..2d224a6310e --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorstatus.go @@ -0,0 +1,58 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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/client-go/applyconfigurations/meta/v1" +) + +// NavigatorStatusApplyConfiguration represents a declarative configuration of the NavigatorStatus type for use +// with apply. +// +// NavigatorStatus defines the observed state of Navigator. +type NavigatorStatusApplyConfiguration struct { + // conditions represent the current state of the Navigator 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. + Conditions []metav1.ConditionApplyConfiguration `json:"conditions,omitempty"` +} + +// NavigatorStatusApplyConfiguration constructs a declarative configuration of the NavigatorStatus type for use with +// apply. +func NavigatorStatus() *NavigatorStatusApplyConfiguration { + return &NavigatorStatusApplyConfiguration{} +} + +// WithConditions adds the given value to the Conditions field in the declarative configuration +// and returns the receiver, so that objects can be build by chaining "With" function invocations. +// If called multiple times, values provided by each call will be appended to the Conditions field. +func (b *NavigatorStatusApplyConfiguration) WithConditions(values ...*metav1.ConditionApplyConfiguration) *NavigatorStatusApplyConfiguration { + for i := range values { + if values[i] == nil { + panic("nil value passed to WithConditions") + } + b.Conditions = append(b.Conditions, *values[i]) + } + return b +} diff --git a/testdata/project-v4/api/v1/applyconfiguration/internal/internal.go b/testdata/project-v4/api/v1/applyconfiguration/internal/internal.go new file mode 100644 index 00000000000..498d3ef4945 --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/internal/internal.go @@ -0,0 +1,256 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 internal + +import ( + fmt "fmt" + sync "sync" + + typed "sigs.k8s.io/structured-merge-diff/v6/typed" +) + +func Parser() *typed.Parser { + parserOnce.Do(func() { + var err error + parser, err = typed.NewParser(schemaYAML) + if err != nil { + panic(fmt.Sprintf("Failed to parse schema: %v", err)) + } + }) + return parser +} + +var parserOnce sync.Once +var parser *typed.Parser +var schemaYAML = typed.YAMLObject(`types: +- name: io.k8s.apimachinery.pkg.apis.meta.v1.Condition + map: + fields: + - name: lastTransitionTime + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time + - name: message + type: + scalar: string + - name: observedGeneration + type: + scalar: numeric + - name: reason + type: + scalar: string + - name: status + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ConditionStatus + - name: type + type: + scalar: string +- name: io.k8s.apimachinery.pkg.apis.meta.v1.ConditionStatus + scalar: string +- name: io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1 + map: + elementType: + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable +- name: io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry + map: + fields: + - name: apiVersion + type: + scalar: string + - name: fieldsType + type: + scalar: string + - name: fieldsV1 + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1 + - name: manager + type: + scalar: string + - name: operation + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsOperationType + - name: subresource + type: + scalar: string + - name: time + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time +- name: io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsOperationType + scalar: string +- name: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta + map: + fields: + - name: annotations + type: + map: + elementType: + scalar: string + - name: creationTimestamp + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time + - name: deletionGracePeriodSeconds + type: + scalar: numeric + - name: deletionTimestamp + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Time + - name: finalizers + type: + list: + elementType: + scalar: string + elementRelationship: associative + - name: generateName + type: + scalar: string + - name: generation + type: + scalar: numeric + - name: labels + type: + map: + elementType: + scalar: string + - name: managedFields + type: + list: + elementType: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry + elementRelationship: atomic + - name: name + type: + scalar: string + - name: namespace + type: + scalar: string + - name: ownerReferences + type: + list: + elementType: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference + elementRelationship: associative + keys: + - uid + - name: resourceVersion + type: + scalar: string + - name: selfLink + type: + scalar: string + - name: uid + type: + namedType: io.k8s.apimachinery.pkg.types.UID +- name: io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference + map: + fields: + - name: apiVersion + type: + scalar: string + - name: blockOwnerDeletion + type: + scalar: boolean + - name: controller + type: + scalar: boolean + - name: kind + type: + scalar: string + - name: name + type: + scalar: string + - name: uid + type: + namedType: io.k8s.apimachinery.pkg.types.UID + elementRelationship: atomic +- name: io.k8s.apimachinery.pkg.apis.meta.v1.Time + scalar: untyped +- name: io.k8s.apimachinery.pkg.types.UID + scalar: string +- name: io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.Admiral + map: + fields: + - name: apiVersion + type: + scalar: string + - name: kind + type: + scalar: string + - name: metadata + type: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta + - name: spec + type: + namedType: io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.AdmiralSpec + - name: status + type: + namedType: io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.AdmiralStatus +- name: io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.AdmiralSpec + map: + fields: + - name: foo + type: + scalar: string +- name: io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.AdmiralStatus + map: + fields: + - name: conditions + type: + list: + elementType: + namedType: io.k8s.apimachinery.pkg.apis.meta.v1.Condition + elementRelationship: associative + keys: + - type +- name: io.k8s.sigs.kubebuilder.testdata.project-v4.api.v1.Navigator + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable +- name: __untyped_atomic_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic +- name: __untyped_deduced_ + scalar: untyped + list: + elementType: + namedType: __untyped_atomic_ + elementRelationship: atomic + map: + elementType: + namedType: __untyped_deduced_ + elementRelationship: separable +`) diff --git a/testdata/project-v4/api/v1/applyconfiguration/utils.go b/testdata/project-v4/api/v1/applyconfiguration/utils.go new file mode 100644 index 00000000000..04b8a1643b2 --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/utils.go @@ -0,0 +1,53 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT 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 applyconfiguration + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + managedfields "k8s.io/apimachinery/pkg/util/managedfields" + v1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1" + apiv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1/applyconfiguration/api/v1" + internal "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1/applyconfiguration/internal" +) + +// ForKind returns an apply configuration type for the given GroupVersionKind, or nil if no +// apply configuration type exists for the given GroupVersionKind. +func ForKind(kind schema.GroupVersionKind) interface{} { + switch kind { + // Group=crew.testproject.org, Version=v1 + case v1.SchemeGroupVersion.WithKind("Admiral"): + return &apiv1.AdmiralApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("AdmiralSpec"): + return &apiv1.AdmiralSpecApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("AdmiralStatus"): + return &apiv1.AdmiralStatusApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("Navigator"): + return &apiv1.NavigatorApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("NavigatorSpec"): + return &apiv1.NavigatorSpecApplyConfiguration{} + case v1.SchemeGroupVersion.WithKind("NavigatorStatus"): + return &apiv1.NavigatorStatusApplyConfiguration{} + + } + return nil +} + +func NewTypeConverter(scheme *runtime.Scheme) managedfields.TypeConverter { + return managedfields.NewSchemeTypeConverter(scheme, internal.Parser()) +} diff --git a/testdata/project-v4/api/v1/groupversion_info.go b/testdata/project-v4/api/v1/groupversion_info.go index ba950f081e3..286c8cf602a 100644 --- a/testdata/project-v4/api/v1/groupversion_info.go +++ b/testdata/project-v4/api/v1/groupversion_info.go @@ -16,6 +16,7 @@ limitations under the License. // Package v1 contains API Schema definitions for the crew v1 API group. // +kubebuilder:object:generate=true +// +kubebuilder:ac:generate=true // +groupName=crew.testproject.org package v1 diff --git a/testdata/project-v4/api/v1/navigator_types.go b/testdata/project-v4/api/v1/navigator_types.go new file mode 100644 index 00000000000..e3af741a40a --- /dev/null +++ b/testdata/project-v4/api/v1/navigator_types.go @@ -0,0 +1,97 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" + "k8s.io/apimachinery/pkg/runtime" +) + +// 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. + +// NavigatorSpec defines the desired state of Navigator +type NavigatorSpec 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 Navigator. Edit navigator_types.go to remove/update + // +optional + Foo *string `json:"foo,omitempty"` +} + +// NavigatorStatus defines the observed state of Navigator. +type NavigatorStatus 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 Navigator 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"` +} + +// +genclient +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Navigator is the Schema for the navigators API +type Navigator struct { + metav1.TypeMeta `json:",inline"` + + // metadata is a standard object metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitzero"` + + // spec defines the desired state of Navigator + // +required + Spec NavigatorSpec `json:"spec"` + + // status defines the observed state of Navigator + // +optional + Status NavigatorStatus `json:"status,omitzero"` +} + +// +kubebuilder:object:root=true + +// NavigatorList contains a list of Navigator +type NavigatorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitzero"` + Items []Navigator `json:"items"` +} + +func init() { + SchemeBuilder.Register(func(s *runtime.Scheme) error { + s.AddKnownTypes(SchemeGroupVersion, &Navigator{}, &NavigatorList{}) + return nil + }) +} diff --git a/testdata/project-v4/api/v1/zz_generated.deepcopy.go b/testdata/project-v4/api/v1/zz_generated.deepcopy.go index 9b7a83bd572..5fed656cdb2 100644 --- a/testdata/project-v4/api/v1/zz_generated.deepcopy.go +++ b/testdata/project-v4/api/v1/zz_generated.deepcopy.go @@ -328,6 +328,107 @@ func (in *FirstMateStatus) DeepCopy() *FirstMateStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Navigator) DeepCopyInto(out *Navigator) { + *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 Navigator. +func (in *Navigator) DeepCopy() *Navigator { + if in == nil { + return nil + } + out := new(Navigator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Navigator) 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 *NavigatorList) DeepCopyInto(out *NavigatorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Navigator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NavigatorList. +func (in *NavigatorList) DeepCopy() *NavigatorList { + if in == nil { + return nil + } + out := new(NavigatorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NavigatorList) 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 *NavigatorSpec) DeepCopyInto(out *NavigatorSpec) { + *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 NavigatorSpec. +func (in *NavigatorSpec) DeepCopy() *NavigatorSpec { + if in == nil { + return nil + } + out := new(NavigatorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NavigatorStatus) DeepCopyInto(out *NavigatorStatus) { + *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 NavigatorStatus. +func (in *NavigatorStatus) DeepCopy() *NavigatorStatus { + if in == nil { + return nil + } + out := new(NavigatorStatus) + 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 diff --git a/testdata/project-v4/cmd/main.go b/testdata/project-v4/cmd/main.go index 6ac63a40db5..8986475c9ec 100644 --- a/testdata/project-v4/cmd/main.go +++ b/testdata/project-v4/cmd/main.go @@ -275,6 +275,13 @@ func main() { os.Exit(1) } } + if err := (&controller.NavigatorReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "Failed to create controller", "controller", "navigator") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/testdata/project-v4/config/crd/bases/crew.testproject.org_navigators.yaml b/testdata/project-v4/config/crd/bases/crew.testproject.org_navigators.yaml new file mode 100644 index 00000000000..f5c42fff17e --- /dev/null +++ b/testdata/project-v4/config/crd/bases/crew.testproject.org_navigators.yaml @@ -0,0 +1,126 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: navigators.crew.testproject.org +spec: + group: crew.testproject.org + names: + kind: Navigator + listKind: NavigatorList + plural: navigators + singular: navigator + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Navigator is the Schema for the navigators 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 Navigator + properties: + foo: + description: foo is an example field of Navigator. Edit navigator_types.go + to remove/update + type: string + type: object + status: + description: status defines the observed state of Navigator + properties: + conditions: + description: |- + conditions represent the current state of the Navigator 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: {} diff --git a/testdata/project-v4/config/crd/kustomization.yaml b/testdata/project-v4/config/crd/kustomization.yaml index fb8a418e3aa..ba6e7d87f41 100644 --- a/testdata/project-v4/config/crd/kustomization.yaml +++ b/testdata/project-v4/config/crd/kustomization.yaml @@ -6,6 +6,7 @@ resources: - bases/crew.testproject.org_firstmates.yaml - bases/crew.testproject.org_sailors.yaml - bases/crew.testproject.org_admirales.yaml +- bases/crew.testproject.org_navigators.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: diff --git a/testdata/project-v4/config/rbac/kustomization.yaml b/testdata/project-v4/config/rbac/kustomization.yaml index 07f0e0c05e5..2446ac3dac7 100644 --- a/testdata/project-v4/config/rbac/kustomization.yaml +++ b/testdata/project-v4/config/rbac/kustomization.yaml @@ -22,6 +22,9 @@ resources: # 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. +- navigator_admin_role.yaml +- navigator_editor_role.yaml +- navigator_viewer_role.yaml - admiral_admin_role.yaml - admiral_editor_role.yaml - admiral_viewer_role.yaml diff --git a/testdata/project-v4/config/rbac/navigator_admin_role.yaml b/testdata/project-v4/config/rbac/navigator_admin_role.yaml new file mode 100644 index 00000000000..889da735dcf --- /dev/null +++ b/testdata/project-v4/config/rbac/navigator_admin_role.yaml @@ -0,0 +1,27 @@ +# 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: navigator-admin-role +rules: +- apiGroups: + - crew.testproject.org + resources: + - navigators + verbs: + - '*' +- apiGroups: + - crew.testproject.org + resources: + - navigators/status + verbs: + - get diff --git a/testdata/project-v4/config/rbac/navigator_editor_role.yaml b/testdata/project-v4/config/rbac/navigator_editor_role.yaml new file mode 100644 index 00000000000..5f27c5fb2b3 --- /dev/null +++ b/testdata/project-v4/config/rbac/navigator_editor_role.yaml @@ -0,0 +1,33 @@ +# 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: navigator-editor-role +rules: +- apiGroups: + - crew.testproject.org + resources: + - navigators + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - crew.testproject.org + resources: + - navigators/status + verbs: + - get diff --git a/testdata/project-v4/config/rbac/navigator_viewer_role.yaml b/testdata/project-v4/config/rbac/navigator_viewer_role.yaml new file mode 100644 index 00000000000..8b347ab042c --- /dev/null +++ b/testdata/project-v4/config/rbac/navigator_viewer_role.yaml @@ -0,0 +1,29 @@ +# 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: navigator-viewer-role +rules: +- apiGroups: + - crew.testproject.org + resources: + - navigators + verbs: + - get + - list + - watch +- apiGroups: + - crew.testproject.org + resources: + - navigators/status + verbs: + - get diff --git a/testdata/project-v4/config/rbac/role.yaml b/testdata/project-v4/config/rbac/role.yaml index 44231645b56..307b27eb250 100644 --- a/testdata/project-v4/config/rbac/role.yaml +++ b/testdata/project-v4/config/rbac/role.yaml @@ -36,6 +36,7 @@ rules: - admirales - captains - firstmates + - navigators - sailors verbs: - create @@ -51,6 +52,7 @@ rules: - admirales/finalizers - captains/finalizers - firstmates/finalizers + - navigators/finalizers - sailors/finalizers verbs: - update @@ -60,6 +62,7 @@ rules: - admirales/status - captains/status - firstmates/status + - navigators/status - sailors/status verbs: - get diff --git a/testdata/project-v4/config/samples/crew_v1_navigator.yaml b/testdata/project-v4/config/samples/crew_v1_navigator.yaml new file mode 100644 index 00000000000..2b0efacb172 --- /dev/null +++ b/testdata/project-v4/config/samples/crew_v1_navigator.yaml @@ -0,0 +1,9 @@ +apiVersion: crew.testproject.org/v1 +kind: Navigator +metadata: + labels: + app.kubernetes.io/name: project-v4 + app.kubernetes.io/managed-by: kustomize + name: navigator-sample +spec: + # TODO(user): Add fields here diff --git a/testdata/project-v4/config/samples/kustomization.yaml b/testdata/project-v4/config/samples/kustomization.yaml index e8ab3fe39ac..0b546ff6b97 100644 --- a/testdata/project-v4/config/samples/kustomization.yaml +++ b/testdata/project-v4/config/samples/kustomization.yaml @@ -5,4 +5,5 @@ resources: - crew_v2_firstmate.yaml - crew_v1_sailor.yaml - crew_v1_admiral.yaml +- crew_v1_navigator.yaml # +kubebuilder:scaffold:manifestskustomizesamples diff --git a/testdata/project-v4/dist/install.yaml b/testdata/project-v4/dist/install.yaml index 65ec8d2e65e..62bbb6f1c5a 100644 --- a/testdata/project-v4/dist/install.yaml +++ b/testdata/project-v4/dist/install.yaml @@ -508,6 +508,132 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.21.0 + name: navigators.crew.testproject.org +spec: + group: crew.testproject.org + names: + kind: Navigator + listKind: NavigatorList + plural: navigators + singular: navigator + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: Navigator is the Schema for the navigators 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 Navigator + properties: + foo: + description: foo is an example field of Navigator. Edit navigator_types.go + to remove/update + type: string + type: object + status: + description: status defines the observed state of Navigator + properties: + conditions: + description: |- + conditions represent the current state of the Navigator 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.21.0 @@ -932,6 +1058,7 @@ rules: - admirales - captains - firstmates + - navigators - sailors verbs: - create @@ -947,6 +1074,7 @@ rules: - admirales/finalizers - captains/finalizers - firstmates/finalizers + - navigators/finalizers - sailors/finalizers verbs: - update @@ -956,6 +1084,7 @@ rules: - admirales/status - captains/status - firstmates/status + - navigators/status - sailors/status verbs: - get @@ -992,6 +1121,77 @@ rules: --- 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-navigator-admin-role +rules: +- apiGroups: + - crew.testproject.org + resources: + - navigators + verbs: + - '*' +- apiGroups: + - crew.testproject.org + resources: + - navigators/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-navigator-editor-role +rules: +- apiGroups: + - crew.testproject.org + resources: + - navigators + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - crew.testproject.org + resources: + - navigators/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-navigator-viewer-role +rules: +- apiGroups: + - crew.testproject.org + resources: + - navigators + verbs: + - get + - list + - watch +- apiGroups: + - crew.testproject.org + resources: + - navigators/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole metadata: labels: app.kubernetes.io/managed-by: kustomize diff --git a/testdata/project-v4/go.mod b/testdata/project-v4/go.mod index aa9267bfbab..471a0ac438b 100644 --- a/testdata/project-v4/go.mod +++ b/testdata/project-v4/go.mod @@ -10,6 +10,7 @@ require ( k8s.io/apimachinery v0.36.0 k8s.io/client-go v0.36.0 sigs.k8s.io/controller-runtime v0.24.0 + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 ) require ( @@ -98,6 +99,5 @@ require ( 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 ) diff --git a/testdata/project-v4/internal/controller/navigator_controller.go b/testdata/project-v4/internal/controller/navigator_controller.go new file mode 100644 index 00000000000..fa19aabbbe7 --- /dev/null +++ b/testdata/project-v4/internal/controller/navigator_controller.go @@ -0,0 +1,63 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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" +) + +// NavigatorReconciler reconciles a Navigator object +type NavigatorReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +kubebuilder:rbac:groups=crew.testproject.org,resources=navigators,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=crew.testproject.org,resources=navigators/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=crew.testproject.org,resources=navigators/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 Navigator 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.24.0/pkg/reconcile +func (r *NavigatorReconciler) 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 *NavigatorReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&crewv1.Navigator{}). + Named("navigator"). + Complete(r) +} diff --git a/testdata/project-v4/internal/controller/navigator_controller_test.go b/testdata/project-v4/internal/controller/navigator_controller_test.go new file mode 100644 index 00000000000..be497faceef --- /dev/null +++ b/testdata/project-v4/internal/controller/navigator_controller_test.go @@ -0,0 +1,84 @@ +/* +Copyright 2026 The Kubernetes authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES 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("Navigator 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 + } + navigator := &crewv1.Navigator{} + + BeforeEach(func() { + By("creating the custom resource for the Kind Navigator") + err := k8sClient.Get(ctx, typeNamespacedName, navigator) + if err != nil && errors.IsNotFound(err) { + resource := &crewv1.Navigator{ + 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.Navigator{} + err := k8sClient.Get(ctx, typeNamespacedName, resource) + Expect(err).NotTo(HaveOccurred()) + + By("Cleanup the specific resource instance Navigator") + Expect(k8sClient.Delete(ctx, resource)).To(Succeed()) + }) + It("should successfully reconcile the resource", func() { + By("Reconciling the created resource") + controllerReconciler := &NavigatorReconciler{ + 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. + }) + }) +})