nolar / kopf

A Python framework to write Kubernetes operators in just a few lines of code
https://kopf.readthedocs.io/
MIT License
2.11k stars 158 forks source link

Update status of CRD #646

Open junglie85 opened 3 years ago

junglie85 commented 3 years ago

Question

I would like to display the number of replicas available in the printer columns of my CRD based on the number of available replicas in a StatefulSet that my operator creates. I can't figure out how to subscribe to events and update my status when they change:

- name: Ready Members
  type: integer
  description: The number of ready members
  jsonPath: .status.readyReplicas
kubectl get mycrd some-resource
NAME                  DESIRED MEMBERS   READY MEMBERS   AGE
some-resource   4                                  99                             34m

I can set the number of replicas in the results delivery:

def update(_**):
    return {'readyReplicas': 99}

However, this means I have to put the readyReplicas under update in my CRD, which prevents me from setting the number of readyReplicas on creation.

I'd like to easily be able to get events from the StatefulSet created by my operator and whenever one of its replicas changes I want to update the status of my CRD at both creation and update. I'm not quite sure how to do that with Kopf and would appreciate some guidance.

Checklist

Keywords

status

nolar commented 3 years ago

Sorry for the late response (just noticed the question). I'd suggest using labels to mark the parent-children relationships:

You can use the patch kwarg to directly decide what to store where:

@kopf.on.create('mycr')
def crt(patch, **_):
    patch.status['readyReplicas'] = 99

@kopf.on.update('mycr')
def upd(patch, status, **_):
    patch.status['readyReplicas'] = status.get('readyReplicas', 0) + 1

Cross-resource updates are not that easy. Kopf does not support that yet. You have to use a client library. E.g., with pykube-ng, it would be like this (I use KopfExample-aka-kex for the parent — to quickly test it):

import kopf
import yaml
import pykube

@kopf.on.create('kex')
def create_children(name, **_):
    obj = yaml.safe_load("""
        apiVersion: apps/v1
        kind: StatefulSet
        spec:
          selector:
            matchLabels:
              app: my # has to match .spec.template.metadata.labels
          serviceName: mysvc
          replicas: 3 # by default is 1
          template:
            metadata:
              labels:
                app: my # has to match .spec.selector.matchLabels
            spec:
              terminationGracePeriodSeconds: 10
              containers:
              - name: nginx
                image: k8s.gcr.io/nginx-slim:0.8
    """)
    kopf.adopt(obj)  # includes namespace, name, existing labels.
    kopf.label(obj, {'mycr': name}, nested=['spec.template'])
    api = pykube.HTTPClient(pykube.KubeConfig.from_env())
    pykube.StatefulSet(api, obj).create()

@kopf.on.create('statefulset', labels={'mycr': kopf.PRESENT})
@kopf.on.update('statefulset', labels={'mycr': kopf.PRESENT}, field='status')
@kopf.on.resume('statefulset', labels={'mycr': kopf.PRESENT})
def child_of_mycr_seen(namespace, labels, status, **_):
    parent_name = labels['mycr']
    replicas = status.get('readyReplicas')
    api = pykube.HTTPClient(pykube.KubeConfig.from_env())
    obj = Kex.objects(api, namespace=namespace).get_by_name(parent_name)
    obj.patch({'status': {'readyReplicas': replicas}})  # Beware of HTTP 404! Ignore?

@kopf.on.delete('statefulset', labels={'mycr': kopf.PRESENT})
def child_of_mycr_gone(namespace, labels, name, old, new, **_):
    parent_name = labels['mycr']
    api = pykube.HTTPClient(pykube.KubeConfig.from_env())
    obj = Kex.objects(api, namespace=namespace).get_by_name(parent_name)
    obj.patch({'status': {'readyReplicas': 0, 'status': 'deleted'}})  # Beware of HTTP 404! Ignore?

class Kex(pykube.objects.NamespacedAPIObject):
    version = "kopf.dev/v1"
    endpoint = "kopfexamples"
    kind = 'KopfExample'

Note a few things:

I've tested it locally — it works; even when the StatefulSet is rescaled.

junglie85 commented 3 years ago

Thanks - I've not had time to check this out yet but appreciate you taking the time to help.