From eb3caa3378077a9f8ac7a49ab4a9f0afa137a910 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Wed, 18 Mar 2026 10:26:28 +0100 Subject: [PATCH 1/6] docs: add: Choosing Between Update, Patch, and Apply --- docs/book/src/SUMMARY.md | 1 + docs/book/src/getting-started.md | 13 +- docs/book/src/reference/reference.md | 3 + docs/book/src/reference/update-patch-apply.md | 184 ++++++++++++++++++ .../secondary-owned-resources.md | 12 ++ 5 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 docs/book/src/reference/update-patch-apply.md diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index fb5ee8e082f..2d2bfa85106 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) + - [Choosing Update, Patch, or Apply](./reference/update-patch-apply.md) - [Raising Events](./reference/raising-events.md) - [Watching Resources](./reference/watching-resources.md) - [Owned Resources](./reference/watching-resources/secondary-owned-resources.md) diff --git a/docs/book/src/getting-started.md b/docs/book/src/getting-started.md index 90683b2b19b..94d3efb27dd 100644 --- a/docs/book/src/getting-started.md +++ b/docs/book/src/getting-started.md @@ -299,6 +299,17 @@ The following example illustrates this process: ... ``` + + Now, you can review the complete controller responsible for managing Custom Resources of the Memcached Kind. This controller ensures that the desired state is maintained in the cluster, making sure that our Memcached instance continues running with the number of replicas specified @@ -460,4 +471,4 @@ implemented for your controller. [deploy-image]: ./plugins/available/deploy-image-plugin-v1-alpha.md [GOPATH-golang-docs]: https://golang.org/doc/code.html#GOPATH [go-modules-blogpost]: https://blog.golang.org/using-go-modules -[autoupdate-plugin]: ./plugins/available/autoupdate-v1-alpha.md \ No newline at end of file +[autoupdate-plugin]: ./plugins/available/autoupdate-v1-alpha.md diff --git a/docs/book/src/reference/reference.md b/docs/book/src/reference/reference.md index 6228ccdfa8a..cccb042aa43 100644 --- a/docs/book/src/reference/reference.md +++ b/docs/book/src/reference/reference.md @@ -5,6 +5,9 @@ Finalizers are a mechanism to execute any custom logic related to a resource before it gets deleted from Kubernetes cluster. + - [Choosing Update, Patch, or Apply](update-patch-apply.md) + Compare the tradeoffs between `Update`, `Patch`, and server-side `Apply` + when writing reconcilers. - [Watching Resources](watching-resources.md) Watch resources in the Kubernetes cluster to be informed and take actions on changes. - [Watching Secondary Resources that are `Owned` ](watching-resources/secondary-owned-resources.md) diff --git a/docs/book/src/reference/update-patch-apply.md b/docs/book/src/reference/update-patch-apply.md new file mode 100644 index 00000000000..c87bf15b485 --- /dev/null +++ b/docs/book/src/reference/update-patch-apply.md @@ -0,0 +1,184 @@ +# Choosing Between Update, Patch, and Apply + +New controller authors often ask for one rule that is always correct. There is no such rule. +The right choice depends on what fields your controller owns, how much of the object it wants +to manage, and how likely it is that other actors also write to that object. + +If you want a default that is rarely wrong, prefer `Patch` with optimistic concurrency. + +## Quick Decision Guide + +| If your controller needs to... | Usually use | Why | +| --- | --- | --- | +| Replace most of an object or most of its `status` after a fresh read | `Update` | Simple, but it writes the full object and conflicts more often | +| Change only a few fields on an existing object | `Patch` | Safer for shared objects and less likely to overwrite unrelated fields | +| Declaratively manage a known subset of fields, often on owned child resources | `Apply` | Good create-or-update flow with field ownership tracked by the API server | + +For controller authors, this is a useful starting point: + +- `Update` is the blunt tool. +- `Patch` is the safest general-purpose tool. +- `Apply` is best when your controller clearly owns specific fields and can send its full intent every time. + +## `Update` + +`Update` uses HTTP `PUT`. You send back the full object for that resource or subresource. + +Use `Update` when: + +- your controller has just read the object and is intentionally writing back most of it +- your controller is effectively the main writer for that object or subresource +- you are comfortable handling `409 Conflict` errors and retrying + +Be careful with `Update` because: + +- it requires a current `resourceVersion` +- it rewrites the whole object, not just the fields you changed +- it can accidentally drop fields your client does not know about +- it conflicts more often when other actors update unrelated fields + +For many controllers, `Status().Update(...)` is still reasonable because the controller is often +the only writer of that `status` subresource. Even then, you should expect conflicts and retry or +requeue when needed. + +## `Patch` + +`Patch` changes only part of an object. For controller code, this is often the best default because +controllers usually change a small number of fields while other actors may be updating the same object. + +Use `Patch` when: + +- you only want to change a few fields +- the object may also be changed by users, admission webhooks, or other controllers +- you want to reduce the chance of overwriting unrelated changes + +For controller-runtime clients, `MergeFrom` is a common choice. To make it safer, add optimistic +concurrency so the patch includes `metadata.resourceVersion`: + +```go +base := deployment.DeepCopy() + +deployment.Spec.Replicas = &size + +if err := r.Patch( + ctx, + deployment, + client.MergeFromWithOptions(base, client.MergeFromWithOptimisticLock{}), +); err != nil { + return ctrl.Result{}, err +} +``` + +This keeps the lost-update protection of `Update` while avoiding a full-object write. + +Use `Patch` especially for: + +- finalizers +- labels and annotations +- small changes to `spec` +- small changes to `status` +- shared secondary resources + +### A Note About Patch Types + +Kubernetes supports multiple patch types. For controllers, the most relevant ones are: + +- JSON merge patch, which controller-runtime uses for `client.MergeFrom(...)` +- Server-Side Apply, covered below + +Avoid relying on Strategic Merge Patch for custom resources served via CRDs. Kubernetes does not +support `application/strategic-merge-patch+json` +([StrategicMergeFrom()](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client#StrategicMergeFrom)) +for APIs defined using `CustomResourceDefinition`. + +Also note that merge patch replaces lists rather than merging them item by item. If you patch a list, +be explicit about the full list value you want. + +For CRDs, markers such as [`+listType` and +`+listMapKey`](./markers/crd-processing.md) describe +schema-level list semantics, which are relevant for [Server-Side Apply merge +behavior](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merge-strategy), not for +JSON merge patch used by `client.MergeFrom(...)`. + +If you want `Create`, `Update`, and non-apply `Patch` requests to fail fast when they contain +unknown fields, see [Strict Field Validation](./strict-field-validation.md). In controller-runtime, +`Apply` requests are already strict. + +## `Apply` + +`Apply` means [Server-Side Apply][k8s-ssa]. Instead of sending a read-modify-write update, your controller +sends its declarative intent and lets the API server merge that intent with the live object. +In this section, `Apply` refers to the Kubernetes API operation over HTTP, not the `kubectl apply` +command-line workflow. + +Use `Apply` when: + +- your controller owns the fields it is writing +- your controller can send the complete desired value for those owned fields on every reconcile +- you want create-or-update behavior without a separate `Get` + +`Apply` is often a good fit for child objects such as: + +- `Deployment` +- `Service` +- `ConfigMap` +- `Job` + +especially when your controller created those resources and clearly owns their managed fields. + +`Apply` is usually a poor fit when: + +- the new value depends on the current live value +- you need read-modify-write behavior +- field ownership is unclear +- users or other controllers are expected to edit the same fields + +When using SSA in a controller: + +- use a stable field manager name +- send the full intent for the fields your controller owns +- force conflicts only for objects your controller truly owns and manages + +`Apply` and [strict field validation](./strict-field-validation.md) are still separate concepts, but +controller-runtime does not expose a separate `fieldValidation` setting for `Apply`. `Apply` +requests are already strict and fail if they contain unknown or duplicate fields. + + + +## Practical Recommendations for New Controller Authors + +For a typical Kubebuilder project: + +- For the primary Custom Resource `spec`: usually do not write it from the controller at all. Users own desired state. +- For the primary Custom Resource `status`: `Status().Update(...)` is fine when your controller is the only status writer. Use `Status().Patch(...)` if you only change part of `status` or expect concurrent writers. +- For finalizers on the primary resource: prefer `Patch`. +- For owned child resources: prefer `Apply` if you want declarative ownership, or `Patch` if the update depends on the current live object. +- For shared resources: prefer `Patch` with optimistic concurrency. + +If you are unsure, start with `Patch` plus optimistic concurrency. It is not always the shortest code, +but it is a strong default for avoiding accidental overwrites. + +## Further Reading + +For the underlying Kubernetes semantics, see: + +- [Kubernetes API Concepts][k8s-api-concepts] +- [Server-Side Apply][k8s-ssa] +- [Strict Field Validation](./strict-field-validation.md) +- [controller-runtime client package][controller-runtime-client] + +[k8s-api-concepts]: https://kubernetes.io/docs/reference/using-api/api-concepts/ +[k8s-ssa]: https://kubernetes.io/docs/reference/using-api/server-side-apply/ +[controller-runtime-client]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client diff --git a/docs/book/src/reference/watching-resources/secondary-owned-resources.md b/docs/book/src/reference/watching-resources/secondary-owned-resources.md index 0ac35fed018..684f0bd8ad9 100644 --- a/docs/book/src/reference/watching-resources/secondary-owned-resources.md +++ b/docs/book/src/reference/watching-resources/secondary-owned-resources.md @@ -134,6 +134,16 @@ func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct } ``` + + ## Watching Secondary Resources To ensure that changes to the secondary resource (such as the `Deployment`) trigger @@ -186,3 +196,5 @@ Note that we are granting permissions to `watch` the resources. [cr-owner-ref-doc]: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/controller/controllerutil#SetOwnerReference [controller-gen]: ./../controller-gen.md [markers]:./../markers/rbac.md +[update-patch-apply]: ../update-patch-apply.md +[strict-field-validation]: ../strict-field-validation.md From e6214f6b8487b93a0fb729b8042875173e706c7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Mon, 13 Apr 2026 11:25:13 +0200 Subject: [PATCH 2/6] docs: remove broken strict-field-validation links --- docs/book/src/reference/update-patch-apply.md | 12 +++++------- .../watching-resources/secondary-owned-resources.md | 1 - 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/book/src/reference/update-patch-apply.md b/docs/book/src/reference/update-patch-apply.md index c87bf15b485..8c219155e52 100644 --- a/docs/book/src/reference/update-patch-apply.md +++ b/docs/book/src/reference/update-patch-apply.md @@ -101,8 +101,8 @@ behavior](https://kubernetes.io/docs/reference/using-api/server-side-apply/#merg JSON merge patch used by `client.MergeFrom(...)`. If you want `Create`, `Update`, and non-apply `Patch` requests to fail fast when they contain -unknown fields, see [Strict Field Validation](./strict-field-validation.md). In controller-runtime, -`Apply` requests are already strict. +unknown fields, use strict field validation. In controller-runtime, `Apply` requests are already +strict. ## `Apply` @@ -139,9 +139,9 @@ When using SSA in a controller: - send the full intent for the fields your controller owns - force conflicts only for objects your controller truly owns and manages -`Apply` and [strict field validation](./strict-field-validation.md) are still separate concepts, but -controller-runtime does not expose a separate `fieldValidation` setting for `Apply`. `Apply` -requests are already strict and fail if they contain unknown or duplicate fields. +`Apply` and strict field validation are still separate concepts, but controller-runtime does not +expose a separate `fieldValidation` setting for `Apply`. `Apply` requests are already strict and +fail if they contain unknown or duplicate fields. @@ -176,7 +175,6 @@ For the underlying Kubernetes semantics, see: - [Kubernetes API Concepts][k8s-api-concepts] - [Server-Side Apply][k8s-ssa] -- [Strict Field Validation](./strict-field-validation.md) - [controller-runtime client package][controller-runtime-client] [k8s-api-concepts]: https://kubernetes.io/docs/reference/using-api/api-concepts/ diff --git a/docs/book/src/reference/watching-resources/secondary-owned-resources.md b/docs/book/src/reference/watching-resources/secondary-owned-resources.md index 684f0bd8ad9..8aa94e3c445 100644 --- a/docs/book/src/reference/watching-resources/secondary-owned-resources.md +++ b/docs/book/src/reference/watching-resources/secondary-owned-resources.md @@ -197,4 +197,3 @@ Note that we are granting permissions to `watch` the resources. [controller-gen]: ./../controller-gen.md [markers]:./../markers/rbac.md [update-patch-apply]: ../update-patch-apply.md -[strict-field-validation]: ../strict-field-validation.md From a219d0a8052b49f512608660d4c4beb5c5ae6b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=BCttler?= Date: Tue, 14 Apr 2026 10:24:39 +0200 Subject: [PATCH 3/6] docs: fix title consistency and Status().Update() typo Align SUMMARY.md and reference.md nav entries with the page H1 ("Choosing Between Update, Patch, and Apply"). Fix missing parentheses on Status().Update() in the secondary-owned-resources aside. Co-Authored-By: Claude Sonnet 4.6 --- docs/book/src/SUMMARY.md | 2 +- docs/book/src/reference/reference.md | 2 +- .../reference/watching-resources/secondary-owned-resources.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/book/src/SUMMARY.md b/docs/book/src/SUMMARY.md index 0156f135222..f51ea556ad6 100644 --- a/docs/book/src/SUMMARY.md +++ b/docs/book/src/SUMMARY.md @@ -67,7 +67,7 @@ - [Generating CRDs](./reference/generating-crd.md) - [Using Finalizers](./reference/using-finalizers.md) - [Good Practices](./reference/good-practices.md) - - [Choosing Update, Patch, or Apply](./reference/update-patch-apply.md) + - [Choosing Between Update, Patch, and Apply](./reference/update-patch-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/reference/reference.md b/docs/book/src/reference/reference.md index cccb042aa43..b18d764171f 100644 --- a/docs/book/src/reference/reference.md +++ b/docs/book/src/reference/reference.md @@ -5,7 +5,7 @@ Finalizers are a mechanism to execute any custom logic related to a resource before it gets deleted from Kubernetes cluster. - - [Choosing Update, Patch, or Apply](update-patch-apply.md) + - [Choosing Between Update, Patch, and Apply](update-patch-apply.md) Compare the tradeoffs between `Update`, `Patch`, and server-side `Apply` when writing reconcilers. - [Watching Resources](watching-resources.md) diff --git a/docs/book/src/reference/watching-resources/secondary-owned-resources.md b/docs/book/src/reference/watching-resources/secondary-owned-resources.md index 8aa94e3c445..699f87947f4 100644 --- a/docs/book/src/reference/watching-resources/secondary-owned-resources.md +++ b/docs/book/src/reference/watching-resources/secondary-owned-resources.md @@ -137,7 +137,7 @@ func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct