Resource specification¶
By-name resource selectors¶
The following notations are supported to specify the resources to be handled.
As a rule of thumb, they are designed to infer a developer’s intentions
as accurately as possible, in a way similar to kubectl semantics.
The resource name is always expected to be the rightmost positional value. The remaining parts are considered as an API group and an API version of the resource — given as either two separate strings, or as one separated by a slash:
import kopf
from typing import Any
@kopf.on.event('kopf.dev', 'v1', 'kopfexamples')
@kopf.on.event('kopf.dev/v1', 'kopfexamples')
@kopf.on.event('apps', 'v1', 'deployments')
@kopf.on.event('apps/v1', 'deployments')
@kopf.on.event('', 'v1', 'pods')
def fn(**_: Any) -> None:
pass
If only one API specification is given (except for v1), it is treated
as an API group, and the preferred API version of that API group is used:
import kopf
from typing import Any
@kopf.on.event('kopf.dev', 'kopfexamples')
@kopf.on.event('apps', 'deployments')
def fn(**_: Any) -> None:
pass
It is also possible to specify the resources with kubectl’s semantics:
import kopf
from typing import Any
@kopf.on.event('kopfexamples.kopf.dev')
@kopf.on.event('deployments.apps')
def fn(**_: Any) -> None:
pass
One exceptional case is v1 as the API specification: it corresponds
to K8s’s legacy core API (before API groups appeared), and is equivalent
to an empty API group name. The following specifications are equivalent:
import kopf
from typing import Any
@kopf.on.event('v1', 'pods')
@kopf.on.event('', 'v1', 'pods')
def fn(**_: Any) -> None:
pass
If neither the API group nor the API version is specified, all resources with that name will match regardless of the API group or version. However, it is reasonable to expect only one:
import kopf
from typing import Any
@kopf.on.event('kopfexamples')
@kopf.on.event('deployments')
@kopf.on.event('pods')
def fn(**_: Any) -> None:
pass
In all examples above, where a resource identifier is expected, it can be any name: plural, singular, kind, or a short name. Since it is impossible to guess which one is which, the name is remembered as-is and later matched against all possible names of the specific resource once it is discovered:
import kopf
from typing import Any
@kopf.on.event('kopfexamples')
@kopf.on.event('kopfexample')
@kopf.on.event('KopfExample')
@kopf.on.event('kex')
@kopf.on.event('StatefulSet')
@kopf.on.event('deployments')
@kopf.on.event('pod')
def fn(**_: Any) -> None:
pass
The resource specification can be more specific on which name to match by using the keyword arguments:
import kopf
from typing import Any
@kopf.on.event(kind='KopfExample')
@kopf.on.event(plural='kopfexamples')
@kopf.on.event(singular='kopfexample')
@kopf.on.event(shortcut='kex')
@kopf.on.event(group='kopf.dev', plural='kopfexamples')
@kopf.on.event(group='kopf.dev', version='v1', plural='kopfexamples')
def fn(**_: Any) -> None:
pass
By-category resource selectors¶
Whole categories of resources can be served, but they must be explicitly specified to avoid unintended consequences:
import kopf
from typing import Any
@kopf.on.event(category='all')
def fn(**_: Any) -> None:
pass
Note that the conventional category all does not actually mean all resources,
but only those explicitly added to this category; some built-in resources
are excluded (e.g. ingresses, secrets).
Catch-all resource selectors¶
To handle all resources in an API group/version, use a special marker instead of the mandatory resource name:
import kopf
from typing import Any
@kopf.on.event('kopf.dev', 'v1', kopf.EVERYTHING)
@kopf.on.event('kopf.dev/v1', kopf.EVERYTHING)
@kopf.on.event('kopf.dev', kopf.EVERYTHING)
def fn(**_: Any) -> None:
pass
As a consequence of the above, to handle every resource in the cluster —which might not be the best idea, but is technically possible— omit the API group/version and use the marker only:
import kopf
from typing import Any
@kopf.on.event(kopf.EVERYTHING)
def fn(**_: Any) -> None:
pass
Serving everything is better when it is used with filters:
import kopf
from typing import Any
@kopf.on.event(kopf.EVERYTHING, labels={'only-this': kopf.PRESENT})
def fn(**_: Any) -> None:
pass
Callable resource selectors¶
To have fine-grained control over which resources are handled,
you can use a single positional callback as the resource specifier.
It must accept one positional argument of type kopf.Resource
and return a boolean indicating whether to handle the resource:
import kopf
from typing import Any
def kex_selector(resource: kopf.Resource) -> bool:
return resource.plural == 'kopfexamples' and resource.preferred
@kopf.on.event(kex_selector)
def fn(**_: Any) -> None:
pass
You can combine the callable resource selectors with other keyword selectors (but not the positional by-name or catch-all selectors):
import kopf
from typing import Any
def kex_selector(resource: kopf.Resource) -> bool:
return resource.plural == 'kopfexamples' and resource.preferred
@kopf.on.event(kex_selector, group='kopf.dev')
def fn(**_: Any) -> None:
pass
There is a subtle difference between callable resource selectors and filters
(see when=… in Filtering): a callable filter applies to all events
coming from a live watch stream identified by a resource kind and a namespace
(or by a resource kind alone for watch streams of cluster-wide operators);
a callable resource selector decides whether to start the watch stream
for that resource kind at all, which can help reduce the load on the API.
Note
Normally, Kopf selects only the “preferred” versions of each API group
when filtered by names. This does not apply to callable selectors.
To handle non-preferred versions, define a callable and return True
regardless of the version or its preferred field.
Exclusion of core v1 events¶
Core v1 events are excluded from EVERYTHING and from callable selectors
regardless of what the selector function returns: events are created during
handling of other resources via the implicit Events from log messages,
so they would cause unnecessary handling cycles for every meaningful change.
To handle core v1 events, name them directly and explicitly:
import kopf
from typing import Any
def all_core_v1(resource: kopf.Resource) -> bool:
return resource.group == '' and resource.preferred
@kopf.on.event(all_core_v1)
@kopf.on.event('v1', 'events')
def fn(**_: Any) -> None:
pass
Multiple resource selectors¶
The resource specifications do not support multiple values, masks, or globs. To handle multiple independent resources, add multiple decorators to the same handler function —as shown above— or use a callable selector. The handlers are deduplicated by the underlying function and its handler id (which equals the function’s name by default unless overridden), so a function will never be triggered multiple times for the same resource even if there are accidental overlaps in the specifications.
import kopf
from typing import Any
@kopf.on.event('kopfexamples')
@kopf.on.event('v1', 'pods')
def fn(**_: Any) -> None:
pass
Ambiguous resource selectors¶
Warning
Kopf tries to make it easy to specify resources in the style of kubectl.
However, some things cannot be made that easy. If resources are specified
ambiguously — i.e. if 2 or more resources from different API groups match
the same resource specification — neither of them will be served,
and a warning will be issued.
This only applies to resource specifications that are intended to match a specific resource by name; specifications with intentional multi-resource mode are served as usual (e.g. by categories).
However, v1 resources have priority over all other resources. This
resolves the conflict between pods.v1 and pods.v1beta1.metrics.k8s.io,
so just "pods" can be specified and the intention will be understood.
This mimics the behavior of kubectl, where such API priorities
are hard-coded.
While it may be convenient to write short forms of resource names, the proper approach is to always include at least an API group:
import kopf
from typing import Any
@kopf.on.event('pods') # NOT SO GOOD, ambiguous, though works
@kopf.on.event('pods.v1') # GOOD, specific
@kopf.on.event('v1', 'pods') # GOOD, specific
@kopf.on.event('pods.metrics.k8s.io') # GOOD, specific
@kopf.on.event('metrics.k8s.io', 'pods') # GOOD, specific
def fn(**_: Any) -> None:
pass
Reserve short forms for prototyping and experimentation, and for ad-hoc operators with custom resources (non-reusable and running in controlled clusters where no other similar resources can be defined).
Warning
Some API groups are served by API extensions, e.g. metrics.k8s.io.
If the extension’s deployment/service/pods are down, such a group will
not be scannable (failing with “HTTP 503 Service Unavailable”)
and will block scanning the entire cluster if resources are specified
without a group name (e.g. ('pods') instead of ('v1', 'pods')).
To avoid scanning the entire cluster and all (even unused) API groups, it is recommended to specify at least the group name for all resources, especially in reusable and publicly distributed operators.