Hierarchies

One of the most common operator patterns is to create child resources in the same Kubernetes cluster. Kopf provides some tools to simplify connecting these resources by manipulating their content before it is sent to the Kubernetes API.

Note

Kopf is not a Kubernetes client library. It does not provide any means to manipulate the Kubernetes resources in the cluster or to directly talk to the Kubernetes API in any other way. Use any of the existing libraries for that purpose, such as the official kubernetes client, pykorm, or pykube-ng.

In all examples below, obj and objs are either a supported object type (native or 3rd-party, see below) or a list, tuple, or iterable containing several objects.

Labels

To label the resources to be created, use kopf.label():

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.label(objs, {'label1': 'value1', 'label2': 'value2'})
    print(objs)
    # [{'kind': 'Job',
    #   'metadata': {'labels': {'label1': 'value1', 'label2': 'value2'}}},
    #  {'kind': 'Deployment',
    #   'metadata': {'labels': {'label1': 'value1', 'label2': 'value2'}}}]

To label the specified resource(s) with the same labels as the resource being processed, omit the labels or set them to None (note that this is not the same as an empty dict {} — which is equivalent to doing nothing):

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.label(objs)
    print(objs)
    # [{'kind': 'Job',
    #   'metadata': {'labels': {'somelabel': 'somevalue'}}},
    #  {'kind': 'Deployment',
    #   'metadata': {'labels': {'somelabel': 'somevalue'}}}]

By default, if any of the requested labels already exist, they will not be overwritten. To overwrite labels, use forced=True:

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.label(objs, {'label1': 'value1', 'somelabel': 'not-this'}, forced=True)
    kopf.label(objs, forced=True)
    print(objs)
    # [{'kind': 'Job',
    #   'metadata': {'labels': {'label1': 'value1', 'somelabel': 'somevalue'}}},
    #  {'kind': 'Deployment',
    #   'metadata': {'labels': {'label1': 'value1', 'somelabel': 'somevalue'}}}]

Nested labels

For some resources, such as Job or Deployment, additional fields must be modified to affect the doubly-nested children (Pod in this case).

To do this, their nested fields must be listed in a nested=[...] iterable. If there is only one nested field, it can be passed directly as nested='...'.

If the nested structures are absent in the target resources, they are skipped and no labels are added. Labels are added only to pre-existing structures:

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment', 'spec': {'template': {}}}]
    kopf.label(objs, {'label1': 'value1'}, nested='spec.template')
    kopf.label(objs, nested='spec.template')
    print(objs)
    # [{'kind': 'Job',
    #   'metadata': {'labels': {'label1': 'value1', 'somelabel': 'somevalue'}}},
    #  {'kind': 'Deployment',
    #   'metadata': {'labels': {'label1': 'value1', 'somelabel': 'somevalue'}},
    #   'spec': {'template': {'metadata': {'labels': {'label1': 'value1', 'somelabel': 'somevalue'}}}}}]

The nested structures are treated as if they were root-level resources, i.e. they are expected to have the metadata structure already, or it will be added automatically.

Nested resources are labelled in addition to the target resources. To label only the nested resources without the root resource, pass them directly to the function (e.g., kopf.label(obj['spec']['template'], ...)).

Owner references

Kubernetes natively supports owner references: a child resource can be marked as “owned” by one or more other resources (owners or parents). If the owner is deleted, its children will be deleted automatically, and no additional handlers are needed.

The owner is a dict containing the fields apiVersion, kind, metadata.name, and metadata.uid (other fields are ignored). This is usually the body from the handler keyword arguments, but you can construct your own dict or obtain one from a 3rd-party client library.

To set the ownership, use kopf.append_owner_reference(). To remove the ownership, use kopf.remove_owner_reference():

owner = {'apiVersion': 'v1', 'kind': 'Pod', 'metadata': {'name': 'pod1', 'uid': '123…'}}
kopf.append_owner_reference(objs, owner)
kopf.remove_owner_reference(objs, owner)

To add or remove ownership of the specified resource(s) by the resource currently being processed, omit the explicit owner argument or set it to None:

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.append_owner_reference(objs)
    print(objs)
    # [{'kind': 'Job',
    #   'metadata': {'ownerReferences': [{'controller': True,
    #      'blockOwnerDeletion': True,
    #      'apiVersion': 'kopf.dev/v1',
    #      'kind': 'KopfExample',
    #      'name': 'kopf-example-1',
    #      'uid': '6b931859-5d50-4b5c-956b-ea2fed0d1058'}]}},
    #  {'kind': 'Deployment',
    #   'metadata': {'ownerReferences': [{'controller': True,
    #      'blockOwnerDeletion': True,
    #      'apiVersion': 'kopf.dev/v1',
    #      'kind': 'KopfExample',
    #      'name': 'kopf-example-1',
    #      'uid': '6b931859-5d50-4b5c-956b-ea2fed0d1058'}]}}]

To set an owner that is not a controller or does not block owner deletion:

kopf.append_owner_reference(objs, controller=False, block_owner_deletion=False)

Both of the above are True by default.

See also

Cascaded deletion.

Names

It is common to name child resources after the parent resource: either exactly as the parent, or with a random suffix.

To assign a name to resource(s), use kopf.harmonize_naming(). If the resource already has its metadata.name field set, that name will be used. If it does not, the specified name will be used. This can be enforced with forced=True:

kopf.harmonize_naming(objs, 'some-name')
kopf.harmonize_naming(objs, 'some-name', forced=True)

By default, the specified name is used as a prefix, and a random suffix is requested from Kubernetes (via metadata.generateName). This is the most common mode when there are multiple child resources of the same kind. To ensure an exact name for single-child cases, pass strict=True:

kopf.harmonize_naming(objs, 'some-name', strict=True)
kopf.harmonize_naming(objs, 'some-name', strict=True, forced=True)

To align the name of the target resource(s) with the name of the resource currently being processed, omit the name or set it to None (both strict=True and forced=True are supported in this form too):

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.harmonize_naming(objs, forced=True, strict=True)
    print(objs)
    # [{'kind': 'Job', 'metadata': {'name': 'kopf-example-1'}},
    #  {'kind': 'Deployment', 'metadata': {'name': 'kopf-example-1'}}]

Alternatively, the operator can request Kubernetes to generate a name with the specified prefix and a random suffix (via metadata.generateName). The actual name will only be known after the resource is created:

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.harmonize_naming(objs)
    print(objs)
    # [{'kind': 'Job', 'metadata': {'generateName': 'kopf-example-1-'}},
    #  {'kind': 'Deployment', 'metadata': {'generateName': 'kopf-example-1-'}}]

Both approaches are commonly used for parent resources that orchestrate multiple child resources of the same kind (e.g., pods in a deployment).

Namespaces

Typically, child resources are expected to be created in the same namespace as their parent (unless there are strong reasons to do otherwise).

To set the desired namespace, use kopf.adjust_namespace():

kopf.adjust_namespace(objs, 'namespace')

If the namespace is already set, it will not be overwritten. To overwrite, pass forced=True:

kopf.adjust_namespace(objs, 'namespace', forced=True)

To align the namespace of the specified resource(s) with the namespace of the resource currently being processed, omit the namespace or set it to None:

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.adjust_namespace(objs, forced=True)
    print(objs)
    # [{'kind': 'Job', 'metadata': {'namespace': 'default'}},
    #  {'kind': 'Deployment', 'metadata': {'namespace': 'default'}}]

Adopting

All of the above can be done in a single call with kopf.adopt(); the forced, strict, and nested flags are passed to all functions that support them:

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    objs = [{'kind': 'Job'}, {'kind': 'Deployment'}]
    kopf.adopt(objs, strict=True, forced=True, nested='spec.template')
    print(objs)
    # [{'kind': 'Job',
    #   'metadata': {'ownerReferences': [{'controller': True,
    #      'blockOwnerDeletion': True,
    #      'apiVersion': 'kopf.dev/v1',
    #      'kind': 'KopfExample',
    #      'name': 'kopf-example-1',
    #      'uid': '4a15f2c2-d558-4b6e-8cf0-00585d823511'}],
    #    'name': 'kopf-example-1',
    #    'namespace': 'default',
    #    'labels': {'somelabel': 'somevalue'}}},
    #  {'kind': 'Deployment',
    #   'metadata': {'ownerReferences': [{'controller': True,
    #      'blockOwnerDeletion': True,
    #      'apiVersion': 'kopf.dev/v1',
    #      'kind': 'KopfExample',
    #      'name': 'kopf-example-1',
    #      'uid': '4a15f2c2-d558-4b6e-8cf0-00585d823511'}],
    #    'name': 'kopf-example-1',
    #    'namespace': 'default',
    #    'labels': {'somelabel': 'somevalue'}}}]

3rd-party libraries

All described methods support resource-related classes from selected libraries in the same way as native Python dictionaries (or any mutable mappings). Currently, these are pykube-ng (classes based on pykube.objects.APIObject) and kubernetes client (resource models from kubernetes.client.models).

import kopf
import pykube
from typing import Any

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    api = pykube.HTTPClient(pykube.KubeConfig.from_env())
    pod = pykube.objects.Pod(api, {})
    kopf.adopt(pod)
import kopf
import kubernetes.client
from typing import Any

@kopf.on.create('KopfExample')
def create_fn(**_: Any) -> None:
    pod = kubernetes.client.V1Pod()
    kopf.adopt(pod)
    print(pod)
    # {'api_version': None,
    #  'kind': None,
    #  'metadata': {'annotations': None,
    #               'cluster_name': None,
    #               'creation_timestamp': None,
    #               'deletion_grace_period_seconds': None,
    #               'deletion_timestamp': None,
    #               'finalizers': None,
    #               'generate_name': 'kopf-example-1-',
    #               'generation': None,
    #               'labels': {'somelabel': 'somevalue'},
    #               'managed_fields': None,
    #               'name': None,
    #               'namespace': 'default',
    #               'owner_references': [{'api_version': 'kopf.dev/v1',
    #                                     'block_owner_deletion': True,
    #                                     'controller': True,
    #                                     'kind': 'KopfExample',
    #                                     'name': 'kopf-example-1',
    #                                     'uid': 'a114fa89-e696-4e84-9b80-b29fbccc460c'}],
    #               'resource_version': None,
    #               'self_link': None,
    #               'uid': None},
    #  'spec': None,
    #  'status': None}