From e91383e4a5c0e4ada0459087662fbcec773637d7 Mon Sep 17 00:00:00 2001 From: Camila Macedo <7708031+camilamacedo86@users.noreply.github.com> Date: Fri, 1 May 2026 12:37:30 +0200 Subject: [PATCH] feat(go/v4): Add support for Server-Side Apply with --ssa flag Add --ssa flag to create api command for scaffolding APIs with Server-Side Apply support. Uses template conditions instead of code injection where possible. --- docs/book/src/SUMMARY.md | 1 + docs/book/src/faq.md | 24 +- docs/book/src/reference/server-side-apply.md | 373 ++++++++++++++++++ internal/cli/alpha/internal/generate.go | 4 + pkg/model/resource/api.go | 10 +- pkg/plugins/golang/options.go | 4 + pkg/plugins/golang/v4/api.go | 8 + pkg/plugins/golang/v4/scaffolds/api.go | 128 ++++++ .../scaffolds/internal/templates/api/group.go | 3 + .../scaffolds/internal/templates/api/types.go | 6 + test/e2e/all/plugin_v4_test.go | 10 + test/e2e/internal/helpers/generate_v4.go | 40 ++ test/testdata/generate.sh | 6 + testdata/project-v4-multigroup/Makefile | 4 +- testdata/project-v4-multigroup/PROJECT | 10 + .../applyconfiguration/internal/internal.go | 61 +++ .../sea-creatures/v1/prawn.go | 247 ++++++++++++ .../sea-creatures/v1/prawnspec.go | 43 ++ .../sea-creatures/v1/prawnstatus.go | 58 +++ .../v1/applyconfiguration/utils.go | 47 +++ .../api/sea-creatures/v1/groupversion_info.go | 45 +++ .../api/sea-creatures/v1/prawn_types.go | 97 +++++ .../sea-creatures/v1/zz_generated.deepcopy.go | 127 ++++++ testdata/project-v4-multigroup/cmd/main.go | 9 + .../sea-creatures.testproject.org_prawns.yaml | 126 ++++++ .../config/crd/kustomization.yaml | 1 + .../config/rbac/kustomization.yaml | 3 + .../config/rbac/role.yaml | 3 + .../rbac/sea-creatures_prawn_admin_role.yaml | 27 ++ .../rbac/sea-creatures_prawn_editor_role.yaml | 33 ++ .../rbac/sea-creatures_prawn_viewer_role.yaml | 29 ++ .../config/samples/kustomization.yaml | 1 + .../samples/sea-creatures_v1_prawn.yaml | 9 + .../project-v4-multigroup/dist/install.yaml | 200 ++++++++++ testdata/project-v4-multigroup/go.mod | 2 +- .../sea-creatures/prawn_controller.go | 63 +++ .../sea-creatures/prawn_controller_test.go | 84 ++++ .../controller/sea-creatures/suite_test.go | 4 + testdata/project-v4/Makefile | 4 +- testdata/project-v4/PROJECT | 10 + .../v1/applyconfiguration/api/v1/admiral.go | 247 ++++++++++++ .../applyconfiguration/api/v1/admiralspec.go | 43 ++ .../api/v1/admiralstatus.go | 58 +++ .../v1/applyconfiguration/api/v1/navigator.go | 247 ++++++++++++ .../api/v1/navigatorspec.go | 43 ++ .../api/v1/navigatorstatus.go | 58 +++ .../applyconfiguration/internal/internal.go | 61 +++ .../api/v1/applyconfiguration/utils.go | 53 +++ .../project-v4/api/v1/groupversion_info.go | 1 + testdata/project-v4/api/v1/navigator_types.go | 97 +++++ .../api/v1/zz_generated.deepcopy.go | 101 +++++ testdata/project-v4/cmd/main.go | 7 + .../crew.testproject.org_navigators.yaml | 126 ++++++ .../project-v4/config/crd/kustomization.yaml | 1 + .../project-v4/config/rbac/kustomization.yaml | 3 + .../config/rbac/navigator_admin_role.yaml | 27 ++ .../config/rbac/navigator_editor_role.yaml | 33 ++ .../config/rbac/navigator_viewer_role.yaml | 29 ++ testdata/project-v4/config/rbac/role.yaml | 3 + .../config/samples/crew_v1_navigator.yaml | 9 + .../config/samples/kustomization.yaml | 1 + testdata/project-v4/dist/install.yaml | 200 ++++++++++ testdata/project-v4/go.mod | 2 +- .../controller/navigator_controller.go | 63 +++ .../controller/navigator_controller_test.go | 84 ++++ 65 files changed, 3550 insertions(+), 11 deletions(-) create mode 100644 docs/book/src/reference/server-side-apply.md create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/internal/internal.go create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawn.go create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnspec.go create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/sea-creatures/v1/prawnstatus.go create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/utils.go create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/groupversion_info.go create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/prawn_types.go create mode 100644 testdata/project-v4-multigroup/api/sea-creatures/v1/zz_generated.deepcopy.go create mode 100644 testdata/project-v4-multigroup/config/crd/bases/sea-creatures.testproject.org_prawns.yaml create mode 100644 testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_admin_role.yaml create mode 100644 testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_editor_role.yaml create mode 100644 testdata/project-v4-multigroup/config/rbac/sea-creatures_prawn_viewer_role.yaml create mode 100644 testdata/project-v4-multigroup/config/samples/sea-creatures_v1_prawn.yaml create mode 100644 testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller.go create mode 100644 testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller_test.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/api/v1/admiral.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralspec.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/api/v1/admiralstatus.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/api/v1/navigator.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorspec.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/api/v1/navigatorstatus.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/internal/internal.go create mode 100644 testdata/project-v4/api/v1/applyconfiguration/utils.go create mode 100644 testdata/project-v4/api/v1/navigator_types.go create mode 100644 testdata/project-v4/config/crd/bases/crew.testproject.org_navigators.yaml create mode 100644 testdata/project-v4/config/rbac/navigator_admin_role.yaml create mode 100644 testdata/project-v4/config/rbac/navigator_editor_role.yaml create mode 100644 testdata/project-v4/config/rbac/navigator_viewer_role.yaml create mode 100644 testdata/project-v4/config/samples/crew_v1_navigator.yaml create mode 100644 testdata/project-v4/internal/controller/navigator_controller.go create mode 100644 testdata/project-v4/internal/controller/navigator_controller_test.go 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..13b7cb5781b --- /dev/null +++ b/docs/book/src/reference/server-side-apply.md @@ -0,0 +1,373 @@ +# Server-Side Apply with --ssa Flag + +The `--ssa` flag scaffolds APIs with controllers that use [Server-Side Apply][server-side-apply], enabling safer field management when resources are shared between your controller and users or other controllers. + +By using this flag, you will get: + +- Standard controller scaffolding (same as go/v4) +- Automatic generation of apply configuration types for type-safe Server-Side Apply +- Makefile integration to generate apply configurations alongside DeepCopy methods +- API markers (`+genclient`, `+kubebuilder:ac:generate=true`) for applyconfiguration generation + +## When to use it? + +Use this flag when: + +- **Multiple controllers manage the same resource**: Your controller manages some fields while other controllers or users manage others +- **Users customize your CRs**: Users add their own labels, annotations, or spec fields that your controller shouldn't overwrite +- **Partial field management**: You only want to manage specific fields and leave others alone +- **Avoiding conflicts**: You want declarative field ownership tracking to prevent accidental overwrites + +**Don't use it when:** +- Your controller is the sole owner of the resource (traditional Update/Patch is simpler) +- You manage the entire object (no shared ownership) +- Simple CRUD operations where you control everything + +## How does it work? + +### Traditional Update vs Server-Side Apply + +**Traditional approach** (without this flag): + +```go +func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var resource myv1.MyResource + if err := r.Get(ctx, req.NamespacedName, &resource); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + // Problem: This overwrites ALL fields, including user customizations + resource.Spec.Replicas = 3 + resource.Labels["managed-by"] = "my-controller" + + if err := r.Update(ctx, &resource); err != nil { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} +``` + +**Server-Side Apply approach** (with this flag): + +```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" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + myv1 "example.com/project/api/apps/v1" +) + +func (r *MyResourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + // Build desired state - only specify fields you want to manage + resource := &myv1.MyResource{ + TypeMeta: metav1.TypeMeta{ + APIVersion: myv1.GroupVersion.String(), + Kind: "MyResource", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: req.NamespacedName.Name, + Namespace: req.NamespacedName.Namespace, + Labels: map[string]string{ + "managed-by": "my-controller", + }, + }, + Spec: myv1.MyResourceSpec{ + Replicas: 3, + }, + } + + // Apply using SSA - only manages the fields specified above + if err := r.Patch(ctx, resource, client.Apply, + client.FieldOwner("my-controller")); err != nil { + if apierrors.IsConflict(err) { + log.Info("Field ownership conflict, will retry", "resource", req.NamespacedName) + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} +``` + +### What gets generated + +1. **API types** with applyconfiguration markers in `api///`: + - `+kubebuilder:ac:generate=true` in `groupversion_info.go` (package-level) + - `+genclient` marker on your CRD type in `*_types.go` + +2. **Standard controller** (same as go/v4): + ```go + func (r *MyKindReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + _ = logf.FromContext(ctx) + + // TODO(user): your logic here + + return ctrl.Result{}, nil + } + ``` + + After running `make generate`, you can import and use the generated applyconfiguration types in your controller. + +3. **Updated Makefile** (first API only): + ```makefile + .PHONY: generate + generate: controller-gen + "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt" \ + applyconfiguration:headerFile="hack/boilerplate.go.txt" paths="./..." + ``` + + The `applyconfiguration` generation is added to the existing object generation command. + +4. **Apply configuration types** (generated by `make generate`): + ``` + applyconfiguration/ + └── // + ├── mykind.go + ├── mykindspec.go + └── mykindstatus.go + ``` + +## How to use it? + +### 1. Initialize your project + +```shell +kubebuilder init --domain example.com --repo example.com/myproject +``` + +### 2. Create API with the flag + +```shell +kubebuilder create api \ + --group apps \ + --version v1 \ + --kind Application \ + --ssa +``` + +### 3. Implement Server-Side Apply in your controller + +After running `make generate`, you can use Server-Side Apply in your controller: + +```go +import ( + 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" + logf "sigs.k8s.io/controller-runtime/pkg/log" + + appsv1 "example.com/myproject/api/apps/v1" +) + +func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := logf.FromContext(ctx) + + // Build desired status - only specify fields you want to manage + app := &appsv1.Application{ + TypeMeta: metav1.TypeMeta{ + APIVersion: appsv1.GroupVersion.String(), + Kind: "Application", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: req.NamespacedName.Name, + Namespace: req.NamespacedName.Namespace, + }, + Status: appsv1.ApplicationStatus{ + Conditions: []metav1.Condition{ + { + Type: "Available", + Status: metav1.ConditionTrue, + Reason: "Reconciled", + LastTransitionTime: metav1.Now(), + }, + }, + }, + } + + // Apply status using SSA + if err := r.Status().Patch(ctx, app, client.Apply, + client.FieldOwner("application-controller")); err != nil { + if apierrors.IsConflict(err) { + log.Info("Field ownership conflict, will retry", "resource", req.NamespacedName) + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil +} +``` + +### 4. Generate and run + +```shell +make manifests generate +make test +make run +``` + +## Mixing with traditional APIs + +You can use this flag for **specific APIs only**. Other APIs in the same project can use traditional Update: + +```shell +# API A - traditional approach (no flag) +kubebuilder create api --group core --version v1 --kind Config + +# API B - with Server-Side Apply flag +kubebuilder create api --group apps --version v1 --kind Workload --ssa + +# API C - traditional approach (no flag) +kubebuilder create api --group core --version v1 --kind Status +``` + +**Result:** +- `Config` and `Status` controllers use traditional Update +- `Workload` controller uses Server-Side Apply +- Only `Workload` has apply configurations generated +- All make targets (`build-installer`, `test`, etc.) work unchanged + +## Affected files + +When using the `create api` command with this flag, the following +files are affected: + +- `api///*_types.go`: Scaffolds the API types (same as standard) +- `internal/controller/*_controller.go`: Scaffolds controller using Server-Side Apply +- `config/crd/bases/*`: Scaffolds CRD (same as standard) +- `config/samples/*`: Scaffolds sample CR (same as standard) +- `Makefile`: Adds apply configuration generation for this API +- `cmd/main.go`: Registers the controller (same as standard) + +## Generated file structure + +After creating an API with this flag: + +``` +api/ +└── apps/v1/ + ├── application_types.go + ├── zz_generated.deepcopy.go + └── applyconfiguration/ # Generated by 'make generate' + └── apps/v1/ + ├── application.go + ├── applicationspec.go + └── applicationstatus.go + +internal/controller/ +└── application_controller.go # Uses Server-Side Apply + +Makefile # Updated with applyconfiguration target +``` + + +## Best practices + +### 1. Always specify FieldOwner + +```go +client.FieldOwner("my-controller-name") +``` + +This identifies your controller in the managed fields and enables proper ownership tracking. + +### 2. Handle conflicts gracefully + +Conflicts occur when another manager owns a field you're trying to manage. This is expected behavior in SSA: + +```go +err := r.Patch(ctx, obj, client.Apply, client.FieldOwner("my-controller")) +if err != nil { + if apierrors.IsConflict(err) { + // Conflict detected - another manager owns the same field + // Best practice: Log and requeue to retry + log.Info("Field ownership conflict detected, will retry", + "resource", req.NamespacedName) + return ctrl.Result{Requeue: true}, nil + } + // Handle other errors + return ctrl.Result{}, err +} +``` + +### 3. Only manage what you need + +```go +// Good - only manage specific fields +appApply := appsv1apply.Application(name, ns). + WithSpec(appsv1apply.ApplicationSpec(). + WithReplicas(3)) + +// Avoid - managing entire spec might conflict with users +appApply := appsv1apply.Application(name, ns). + WithSpec(spec) // Don't set the entire spec +``` + +### 4. Use ForceOwnership sparingly + +```go +client.ForceOwnership // Takes ownership even if another manager owns the field +``` + +**Only use when your controller is the authoritative source for specific fields.** + +ForceOwnership defeats the purpose of Server-Side Apply by forcefully taking ownership +from other managers. Reserve it for cases where your controller must have final say over +certain fields (e.g., a parent controller that should always override child controllers). + +Example when ForceOwnership is appropriate: +```go +// A cluster-wide policy controller that must enforce security settings +if err := r.Apply(ctx, deployment, client.Apply, + client.ForceOwnership, // Policy controller has final authority + client.FieldOwner("security-policy-controller")); err != nil { + return ctrl.Result{}, err +} +``` + +## Additional resources + +For more details on Server-Side Apply concepts and patterns, see: + +- [Kubernetes Server-Side Apply Documentation][server-side-apply] +- [controller-gen CLI Reference](./controller-gen.md) + +## Example in Testdata + +To see examples of SSA API scaffolds, check the testdata samples: + +**Single-group project:** [testdata/project-v4][testdata-single] +- [`api/v1/navigator_types.go`][testdata-single-types] - API with `+genclient` marker +- [`api/v1/groupversion_info.go`][testdata-single-gv] - Package with `+kubebuilder:ac:generate=true` +- [`internal/controller/navigator_controller.go`][testdata-single-controller] - Standard controller template +- [`api/v1/applyconfiguration/`][testdata-single-ac] - Generated apply configurations + +**Multi-group project:** [testdata/project-v4-multigroup][testdata-multi] +- [`api/sea-creatures/v1/prawn_types.go`][testdata-multi-types] - API with `+genclient` marker +- [`api/sea-creatures/v1/groupversion_info.go`][testdata-multi-gv] - Package with `+kubebuilder:ac:generate=true` +- [`internal/controller/sea-creatures/prawn_controller.go`][testdata-multi-controller] - Standard controller template +- [`api/sea-creatures/v1/applyconfiguration/`][testdata-multi-ac] - Generated apply configurations + +[testdata-single]: ./../../../../testdata/project-v4 +[testdata-single-types]: ./../../../../testdata/project-v4/api/v1/navigator_types.go +[testdata-single-gv]: ./../../../../testdata/project-v4/api/v1/groupversion_info.go +[testdata-single-controller]: ./../../../../testdata/project-v4/internal/controller/navigator_controller.go +[testdata-single-ac]: ./../../../../testdata/project-v4/api/v1/applyconfiguration/ +[testdata-multi]: ./../../../../testdata/project-v4-multigroup +[testdata-multi-types]: ./../../../../testdata/project-v4-multigroup/api/sea-creatures/v1/prawn_types.go +[testdata-multi-gv]: ./../../../../testdata/project-v4-multigroup/api/sea-creatures/v1/groupversion_info.go +[testdata-multi-controller]: ./../../../../testdata/project-v4-multigroup/internal/controller/sea-creatures/prawn_controller.go +[testdata-multi-ac]: ./../../../../testdata/project-v4-multigroup/api/sea-creatures/v1/applyconfiguration/ + +[controller-runtime]: https://github.com/kubernetes-sigs/controller-runtime +[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..2ece01f4f17 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,117 @@ 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. Add applyconfiguration generation manually for SSA", + "path", makefilePath, "error", err) + return + } + if hasMarker { + return + } + + // Try multiple patterns to handle different Makefile formats + // 1. Try with boilerplate and YEAR variable (current default) + //nolint:lll + err = util.ReplaceInFile(makefilePath, makefileOldObjectGenWithBoilerplateAndYear, makefileNewObjectGenWithBoilerplateAndYear) + if err != nil { + // 2. Try with boilerplate but without YEAR (legacy) + err = util.ReplaceInFile(makefilePath, makefileOldObjectGenWithBoilerplate, makefileNewObjectGenWithBoilerplate) + if err != nil { + // 3. Try without boilerplate + err = util.ReplaceInFile(makefilePath, makefileOldObjectGenNoBoilerplate, makefileNewObjectGenNoBoilerplate) + if err != nil { + log.Warn("unable to find standard controller-gen object generator line in Makefile. "+ + "Add applyconfiguration generation manually", + "error", err) + return + } + } + } + + log.Info("applyconfiguration generation added to Makefile generate target") +} 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..2ca63605d90 100644 --- a/test/e2e/internal/helpers/generate_v4.go +++ b/test/e2e/internal/helpers/generate_v4.go @@ -240,6 +240,46 @@ 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()) +} + 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 8e3f9df9bee..ac0ac81c059 100644 --- a/testdata/project-v4-multigroup/Makefile +++ b/testdata/project-v4-multigroup/Makefile @@ -48,8 +48,8 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate -generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..." +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, DeepCopyObject, and applyconfiguration implementations. + "$(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..195bb5d8e36 --- /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.20.1 + 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 74241c6ac20..52e7228e557 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.20.1 + 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 3ab3e541ae2..df6564aac8d 100644 --- a/testdata/project-v4/Makefile +++ b/testdata/project-v4/Makefile @@ -48,8 +48,8 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust "$(CONTROLLER_GEN)" rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate -generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. - "$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..." +generate: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, DeepCopyObject, and applyconfiguration implementations. + "$(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..702c739db6e --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/admiral.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" +) + +// 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, namespace string) *AdmiralApplyConfiguration { + b := &AdmiralApplyConfiguration{} + b.WithName(name) + b.WithNamespace(namespace) + b.WithKind("Admiral") + b.WithAPIVersion("crew.testproject.org/v1") + return b +} + +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..4cb98ac9b65 --- /dev/null +++ b/testdata/project-v4/api/v1/applyconfiguration/api/v1/navigator.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" +) + +// 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 +} + +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..ed7dc0ff1e0 --- /dev/null +++ b/testdata/project-v4/api/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/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..dd2c0b5b9ad --- /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.20.1 + 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 db95925437a..ff87f1e977f 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.20.1 + 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.20.1 @@ -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. + }) + }) +})