Patching¶
Handlers can modify the Kubernetes resource they are handling
by using the patch keyword argument.
There are two patching strategies available:
the merge-patch dictionary for simple field changes,
and transformation functions for operations that depend
on the current state of the resource, such as list manipulations.
The changes from both strategies are mixed with the framework’s own modifications (such as progress storage, finalizer management, and handler result delivery) and applied together in the minimal number of API calls (depending on the resource definition).
There can be anywhere from zero to four patches per processing cycle, depending on whether the status is a subresource, and whether the patch is a mix of dictionary changes and transformation functions with actual changes.
Alternatively, operator developers can use any third-party
Kubernetes client library to patch their resources directly
inside the handlers instead of using the provided patch facility.
Dictionary merge-patches¶
The patch object behaves as a mutable dictionary.
The changes accumulated in it are applied to the resource
as a JSON merge-patch (application/merge-patch+json)
after the handler finishes:
import kopf
from typing import Any
@kopf.on.create('kopfexamples')
def ensure_defaults(spec: kopf.Spec, patch: kopf.Patch, **_: Any) -> None:
if 'greeting' not in spec:
patch.spec['greeting'] = 'hello'
patch.status['state'] = 'initialized'
Setting a field to None deletes it from the resource.
Nested dictionaries are merged recursively.
Other values overwrite the existing ones:
import kopf
from typing import Any
@kopf.on.update('kopfexamples')
def cleanup_obsolete_fields(patch: kopf.Patch, **_: Any) -> None:
patch.spec['obsoleteField'] = None # deletes the field
patch.status['phase'] = 'updated' # overwrites the value
Transformation functions¶
Some changes cannot be expressed as a merge patch. In particular, list operations (appending to or removing from a list) require knowing the current state of the list to calculate the correct indices. For example, adding a finalizer requires knowing how many finalizers are already present; removing one requires knowing its position in the list.
For these cases, the patch object provides the patch.fns property.
You can append any function of type Callable[[dict], None] to patch.fns
(or insert into the beginning or the middle of the list, if that matters).
Each function accepts the raw resource body as a positional argument
(a regular mutable dictionary) and mutates it in place.
The dictionary being mutated is already a deep copy of the original body,
so there is no need to worry:
import kopf
from typing import Any
def add_finalizer(body: kopf.RawBody, /) -> None:
finalizers = body.setdefault('metadata', {}).setdefault('finalizers', [])
if 'my-operator/cleanup' not in finalizers:
finalizers.append('my-operator/cleanup')
@kopf.on.create('kopfexamples')
def create_fn(patch: kopf.Patch, **_: Any) -> None:
patch.fns.append(add_finalizer)
The framework calls the transformation functions against the freshest seen
resource body and computes a JSON diff (application/json-patch+json)
relative to that body.
The resulting JSON Patch operations are sent to the Kubernetes API
with an optimistic concurrency check on the metadata.resourceVersion.
Note
The body passed to the transformation function is the latest version of the resource known to the framework at the time the function is applied. It may already reflect the results of earlier patch operations in the same or previous processing cycles, so it is not necessarily the body from the event that triggered the handler.
The transformation functions may be called more than once across the same or several processing cycles — for instance, if the API server rejects the patch due to a conflict. The functions should therefore be safe to call repeatedly: they should check the current state before making changes rather than assuming a particular initial state.
The transformation function takes a single positional argument
for the body. If additional positional or keyword arguments are needed,
use functools.partial:
import functools
import kopf
from typing import Any
def set_label(body: kopf.RawBody, /, name: str, value: str) -> None:
body.setdefault('metadata', {}).setdefault('labels', {})[name] = value
@kopf.on.create('kopfexamples')
def create_fn(patch: kopf.Patch, **_: Any) -> None:
patch.fns.append(functools.partial(set_label, name='my-label', value='my-value'))
Patch timing in daemons and timers¶
For daemons and timers, the patch
is applied after the handler exits on each iteration of the run loop —
including when the handler raises kopf.TemporaryError for retrying.
After the patch is applied, it is cleared for the next iteration.
This means any changes accumulated in the patch dictionary
and any transformation functions appended to patch.fns
during the handler’s execution are sent to the Kubernetes API
before the next invocation of the handler starts.
If a transformation function’s JSON Patch is rejected by the API server
due to an optimistic concurrency conflict (HTTP 422), the transformation
functions are carried forward to the next iteration, where they are
retried against the newer state of the resource. The retry does not happen
in the background — it waits until the handler is invoked again on the next
timer interval or daemon retry. Handlers can detect carried-forward
transformation functions by checking bool(patch) at the start of the
handler: if it is true before the handler has made any changes, it means
there are pending transformation functions from a previous iteration.