Hierarchies

One of the most common patterns of the operators is to create children 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/iterable with several objects.

Labels

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

@kopf.on.create('KopfExample')
def create_fn(**_):
    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 at the moment, omit the labels or set them to None (note, it is not the same as an empty dict {} – which is equivalent to doing nothing):

@kopf.on.create('KopfExample')
def create_fn(**_):
    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 some of the requested labels already exist, they will not be overwritten. To overwrite labels, use forced=True:

@kopf.on.create('KopfExample')
def create_fn(**_):
    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, e.g. Job or Deployment, additional fields have to be modified to affect the double-nested children (Pod in this case).

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

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

@kopf.on.create('KopfExample')
def create_fn(**_):
    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 the root-level resources, i.e. they are expected to have or automatically get the metadata structure added.

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

Owner references

Kubernetes natively supports the 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 too, automatically, and no additional handlers are needed.

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

kopf.append_owner_reference(objs, owner)
kopf.remove_owner_reference(objs, owner)

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

@kopf.on.create('KopfExample')
def create_fn(**_):
    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'}]}}]

See also

Cascaded deletion.

Names

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

To give the resource(s) a name, use kopf.harmonize_naming(). If the resource has its metadata.name field set, that name will be used. If it does not, the specified name will be used. It 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 widely used mode with multiple children resource of the same kind. To ensure the 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 being processed at the moment, 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(**_):
    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 be known only after the resource is created:

@kopf.on.create('KopfExample')
def create_fn(**_):
    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 ways are commonly used for parent resources that orchestrate multiple children resources of the same kind (e.g., pods in the deployment).

Namespaces

Usually, it is expected that the children resources are created in the same namespace as their parent (unless there are strong reasons to do differently).

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 being processed, omit the namespace or set it to None:

@kopf.on.create('KopfExample')
def create_fn(**_):
    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 one call with kopf.adopt(); forced, strict, nested flags are passed to all functions that support them:

@kopf.on.create('KopfExample')
def create_fn(**_):
    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 of selected libraries the same way as the native Python dictionaries (or any mutable mappings). Currently, that is pykube-ng (classes based on pykube.objects.APIObject) and kubernetes client (resource models from kubernetes.client.models).

import kopf
import pykube

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

@kopf.on.create('KopfExample')
def create_fn(**_):
    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}