Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
24 changes: 20 additions & 4 deletions docs/book/src/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand All @@ -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.

<aside class="note warning" role="note">
<p class="note-title">Server-Side Apply Flag vs kubectl Server-Side Apply</p>

Don't confuse the `kubectl apply --server-side` command mentioned above with the `--ssa` flag in Kubebuilder. They are different concepts:

- **`kubectl apply --server-side`** (discussed above): A kubectl flag that changes how CRDs are installed to avoid the annotation size limit. This is about CRD installation only.

- **`--ssa` flag**: A Kubebuilder flag (e.g., `kubebuilder create api --ssa`) that scaffolds controllers with runtime behavior for managing resource fields using Server-Side Apply patterns. This is about controller runtime behavior, specifically for scenarios where your controller shares field ownership with users or other controllers. It does NOT change how `make install` applies CRDs.

See the [Kubernetes Server-Side Apply documentation][k8s-ssa-docs] to learn more about the Server-Side Apply concept in general.
</aside>

## 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.
Expand Down Expand Up @@ -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
[k8s-ssa-docs]: https://kubernetes.io/docs/reference/using-api/server-side-apply/
99 changes: 99 additions & 0 deletions docs/book/src/reference/server-side-apply.md
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we specify if there's an easy path to "enable" the plugin on existing APIs? And if there's not maybe simply add a note that suggests that a person willing to do that should regenerate their API and migrate manually?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offen in kubebuilder for we migrate something, we would need to re-scaffold. In this case, I think the best would be to re-scaffold using the plugin, or add the markers in the API and update the makefile.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can study a option to migrate manually and using AI, for example, but then it is out os scope of this one where we need to define how to introduce the feature.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's totally fine and probably not too hard to rescaffold and manually "merge" the scaffolding

Comment thread
camilamacedo86 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Server-Side Apply

The `--ssa` flag scaffolds APIs with [Server-Side Apply][server-side-apply] support, enabling safer field management when multiple actors modify the same resources.

This adds:
- API markers (`+genclient`, `+kubebuilder:ac:generate=true`) for ApplyConfiguration generation
- Makefile integration to generate type-safe ApplyConfiguration types alongside DeepCopy methods

## When to use it

Use Server-Side Apply when:
- Multiple controllers or users manage the same resource
- Users customize CRs with labels, annotations, or spec fields your controller shouldn't overwrite
- You want declarative field ownership tracking
- Other operators will manage instances of your CRs (they can import your generated ApplyConfiguration types)

<aside class="note" role="note">
<p class="note-title">Note</p>

For controllers that are the sole owner of a resource and manage the entire object, traditional `Update()` is simpler and sufficient. Use Server-Side Apply when field ownership matters.

</aside>

## How it works

Traditional `Update()` overwrites the entire object. Server-Side Apply with `client.Apply()` only manages the fields you specify.

After running `make generate`, ApplyConfiguration types are created at:
```
api/<group>/<version>/applyconfiguration/<group>/<version>/
```

Import them as:
```go
appsv1apply "example.com/myproject/api/apps/v1/applyconfiguration/apps/v1"
```

## Usage

Create an API with the `--ssa` flag:

```shell
kubebuilder create api --group apps --version v1 --kind Application --ssa
```

Implement Server-Side Apply in your controller:

```go
import (
"context"

apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"

appsv1apply "example.com/myproject/api/apps/v1/applyconfiguration/apps/v1"
)

func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Build desired state - specify only fields you manage
app := appsv1apply.Application(req.Name, req.Namespace).
WithStatus(appsv1apply.ApplicationStatus().
WithConditions(metav1.Condition{
Type: "Available",
Status: metav1.ConditionTrue,
Reason: "Reconciled",
}))

// Apply status
if err := r.SubResource("status").Apply(ctx, app, client.FieldOwner("application-controller")); err != nil {
if apierrors.IsConflict(err) {
return ctrl.Result{Requeue: true}, nil
}
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
```

Then run:
```shell
make generate manifests
```


## Best practices

- Always specify `client.FieldOwner("controller-name")` to identify your controller
- Handle conflicts by checking `apierrors.IsConflict(err)` and requeueing
- Only specify fields your controller manages using the builder pattern
- Use `client.ForceOwnership` only when your controller must have final authority

## Additional resources

- [Kubernetes Server-Side Apply Documentation][server-side-apply]
- [controller-gen CLI Reference](./controller-gen.md)

[server-side-apply]: https://kubernetes.io/docs/reference/using-api/server-side-apply/
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoelSpeed @alvaroaleman @sbueringer

Could you please give a hand on the review of this doc?
Could you please let us know if has any info here that is not accurate or can/should be shaped?

4 changes: 4 additions & 0 deletions internal/cli/alpha/internal/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion pkg/model/resource/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
4 changes: 4 additions & 0 deletions pkg/plugins/golang/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/plugins/golang/v4/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 {
Expand Down
Loading