kopf icon indicating copy to clipboard operation
kopf copied to clipboard

Question: What is the best approach to monitor children objects within their parent?

Open xocoatzin opened this issue 4 years ago • 2 comments

Hi all,

I'm currently working on porting some code from metacontroller into Kopf.

Metacontroller, gives you the option to receive callbacks when the monitored object changes, or any of its children is updated.

For example, if I'm watching an object of kind multijob, which creates an arbitrary number of standard kubernetes jobs, I would receive a callback if the children fail, restart, succeed, etc, which I can use to update the status field in the parent.

I haven't been able to find a clean way to do the same thing in Kopf, other than adding separate listeners for both the parent/children, and within the children listeners update the parent CRD. Of course the example here is simplified, the actual application would have many dependencies and larger hierarchies of objects, and having this kind of inter-dependencies between listeners make them harder to maintain.

Is there any better way to do this? Or is there any feature in Kopf that would make the management of children easier/cleaner?

Thanks!

xocoatzin avatar Feb 26 '20 11:02 xocoatzin

Related: #58 #264 See also: https://github.com/zalando-incubator/kopf/issues/264#issuecomment-562845724

You are right, the only way is —as you said— "adding separate listeners for both the parent/children, and within the children listeners update the parent CRD".

Keep in mind, that Kopf keeps one and only one watch-query (an API request) per resource kind no matter how many handlers are there for that resource kind. So, there should be no problems with APIs.

There is no simpler (i.e. few-liner) solution at the moment.

A better solution is planned though — but rather later than sooner (because: priorities; and my regular employment takes time).

Under the hood, it will be working exactly the same way, just with better DSL for handlers. Some ideation was happening in this gist.


For all those looking for a solution/pattern and coming to this issue — here is an example, which we currently use for ourselves:

  • Label the children resources with name & namespace of the parent object (assuming they can be in different namespaces; if it is the same namespace by design, only the name is needed).

  • Watch for children resources that have this label (any value). In the watcher, get the name of the parent resource and patch its status field (e.g. status.subpods) with the status of the watched children resource (selected or agregated).

  • Back in the parent resource, react to changes in that status field, and do the calculation on all the children overall statuses.

A sample skeleton code:

# pip install kopf pykube-ng PyYAML
import kopf
import pykube
import yaml


class KopfExample(pykube.objects.NamespacedAPIObject):
    version = "zalando.org/v1"
    endpoint = "kopfexamples"
    kind = "KopfExample"


@kopf.on.create('zalando.org', 'v1', 'kopfexamples')
def spawn_children(name, **_):
    data = yaml.safe_load(f"""
        apiVersion: v1
        kind: Pod
        spec:
          containers:
          - name: the-only-one
            image: busybox
            command: ["sh", "-x", "-c", "sleep 1"]
    """)

    kopf.adopt(data)
    kopf.label(data, labels={'kex-parent-name': name})  # << HERE!

    api = pykube.HTTPClient(pykube.KubeConfig.from_env())
    for _ in range(5):
        pykube.Pod(api, data).create()


@kopf.on.event('', 'v1', 'pods', labels={'kex-parent-name': None})
def kexed_pod_monitoring(meta, name, namespace, status, **_):
    parent_name = meta['labels']['kex-parent-name']

    try:
        api = pykube.HTTPClient(pykube.KubeConfig.from_env())
        parent_kex = KopfExample.objects(api, namespace=namespace).get_by_name(parent_name)
        parent_kex.patch({'status': {'subpods': {name: status['phase']}}})  # << HERE
    except pykube.exceptions.ObjectDoesNotExist:
        pass


@kopf.on.field('zalando.org', 'v1', 'kopfexamples', field='status.subpods')
def kex_subpods_reaction(old, new, diff, **_):
    pass  # << HERE, decide something on ALL of them at once.
    msg = " // ".join([f"{pod_name} is {pod_phase}"
                       for pod_name, pod_phase in new.items()])
    print(f"==> {msg}")

nolar avatar Mar 06 '20 11:03 nolar

Thank you @nolar for the detailed update. My current approach looks very similar to the example you provided. Will follow up closely on future releases.

xocoatzin avatar Mar 06 '20 13:03 xocoatzin