diff --git a/docs/walkthrough/creation.rst b/docs/walkthrough/creation.rst index 97fbcaa9..3d467914 100644 --- a/docs/walkthrough/creation.rst +++ b/docs/walkthrough/creation.rst @@ -10,15 +10,15 @@ We want to create a real ``PersistentVolumeClaim`` object immediately when an ``EphemeralVolumeClaim`` is created this way: .. code-block:: yaml - :name: evc - :caption: evc.yaml + :name: evc + :caption: evc.yaml apiVersion: zalando.org/v1 kind: EphemeralVolumeClaim metadata: name: my-claim spec: - size: 10G + size: 1G .. code-block:: bash @@ -28,8 +28,8 @@ First, let's define a template of the persistent volume claim (with the Python template string, so that no extra template engines are needed): .. code-block:: yaml - :name: pvc - :caption: pvc.yaml + :name: pvc + :caption: pvc.yaml apiVersion: v1 kind: PersistentVolumeClaim @@ -46,13 +46,14 @@ First, let's define a template of the persistent volume claim Let's extend our only handler. -We will use the official Kubernetes client library: +We will use the official Kubernetes client library (``pip install kubernetes``): .. code-block:: python :name: creation :linenos: :caption: ephemeral.py + import os import kopf import kubernetes import yaml @@ -92,5 +93,14 @@ Wait 1-2 seconds, and take a look: Now, the PVC can be attached to the pods by the same name, as EVC is named. +.. note:: + If you have to re-run the operator, and hit a HTTP 409 error saying + "persistentvolumeclaims "my-claim" already exists", + then remove it manually: + + .. code-block:: bash + + kubectl delete pvc my-claim + .. seealso:: See also :doc:`/handlers`, :doc:`/errors`, :doc:`/hierarchies`. diff --git a/docs/walkthrough/deletion.rst b/docs/walkthrough/deletion.rst index 38d9fb4b..a614c657 100644 --- a/docs/walkthrough/deletion.rst +++ b/docs/walkthrough/deletion.rst @@ -30,24 +30,25 @@ __ https://kubernetes.io/docs/concepts/workloads/controllers/garbage-collection/ Let's extend the creation handler: .. code-block:: python - :name: adopting - :linenos: - :caption: ephemeral.py - :emphasize-lines: 18 + :name: adopting + :linenos: + :caption: ephemeral.py + :emphasize-lines: 18 + import os import kopf import kubernetes import yaml @kopf.on.create('zalando.org', 'v1', 'ephemeralvolumeclaims') - def create_fn(meta, spec, namespace, logger, **kwargs): + def create_fn(meta, spec, namespace, logger, body, **kwargs): name = meta.get('name') size = spec.get('size') if not size: raise kopf.PermanentError(f"Size must be set. Got {size!r}.") - path = os.path.join(os.path.dirname(__file__), 'pvc-tpl.yaml') + path = os.path.join(os.path.dirname(__file__), 'pvc.yaml') tmpl = open(path, 'rt').read() text = tmpl.format(name=name, size=size) data = yaml.safe_load(text) @@ -62,7 +63,9 @@ Let's extend the creation handler: logger.info(f"PVC child is created: %s", obj) -With this one line, `kopf.adopt` marks the PVC as child of EVC. + return {'pvc-name': obj.metadata.name} + +With this one line, `kopf.adopt` marks the PVC as a child of EVC. This includes: the name auto-generation (if absent), the label propagation, the namespace assignment to the parent's object namespace, and, finally, the owner referencing. diff --git a/docs/walkthrough/diffs.rst b/docs/walkthrough/diffs.rst index 39ebccaa..d9675cae 100644 --- a/docs/walkthrough/diffs.rst +++ b/docs/walkthrough/diffs.rst @@ -32,9 +32,9 @@ but we will use another feature of Kopf to track one specific field only: :emphasize-lines: 1, 5 @kopf.on.field('zalando.org', 'v1', 'ephemeralvolumeclaims', field='metadata.labels') - def relabel(old, new, **kwargs): + def relabel(old, new, status, namespace, **kwargs): - pvc_name = status['pvc-name'] + pvc_name = status['create_fn']['pvc-name'] pvc_patch = {'metadata': {'labels': new}} api = kubernetes.client.CoreV1Api() @@ -73,7 +73,7 @@ A diff-object has this structure (as an example):: [('add', ('metadata', 'labels', 'label1'), None, 'new-value'), ('change', ('metadata', 'labels', 'label2'), 'old-value', 'new-value'), ('remove', ('metadata', 'labels', 'label3'), 'old-value', None), - ('change', ('spec', 'size'), '10G', '100G')] + ('change', ('spec', 'size'), '1G', '2G')] For the field-handlers, it will be the same, just the field path will be relative to the handled field, @@ -95,10 +95,10 @@ exactly as needed for the patch object (i.e. the field is present there): :emphasize-lines: 4 @kopf.on.field('zalando.org', 'v1', 'ephemeralvolumeclaims', field='metadata.labels') - def relabel(diff, **kwargs): + def relabel(diff, status, namespace, **kwargs): labels_patch = {field[0]: new for op, field, old, new in diff} - pvc_name = status['pvc-name'] + pvc_name = status['create_fn']['pvc-name'] pvc_patch = {'metadata': {'labels': labels_patch}} api = kubernetes.client.CoreV1Api() @@ -112,3 +112,16 @@ Note that the unrelated labels that were put on the PVC ---e.g., manually, from the template, by other controllers/operators, beside the labels coming from the parent EVC--- are persisted and never touched (unless the same-named label is applied to EVC and propagated to the PVC). + +.. code-block:: bash + + kubectl describe pvc my-claim + +.. code-block:: none + + Name: my-claim + Namespace: default + StorageClass: standard + Status: Bound + Labels: application=some-app + owner=me diff --git a/docs/walkthrough/prerequisites.rst b/docs/walkthrough/prerequisites.rst index d310565d..b799cd17 100644 --- a/docs/walkthrough/prerequisites.rst +++ b/docs/walkthrough/prerequisites.rst @@ -10,3 +10,11 @@ Otherwise, let's install minikube locally (e.g. for MacOS): * `Install kubectl `_ * :doc:`Install minikube ` (a local Kubernetes cluster) * :doc:`Install Kopf ` + +.. warning:: + Unfortunately, Minikube cannot handle the PVC/PV resizing, + as it uses the HostPath provider internally. + You can either skip the :doc:`updates` step of this tutorial + (where the sizes of the volumes are changed), + or you can use an external Kubernetes cluster + with real dynamically sized volumes. diff --git a/docs/walkthrough/problem.rst b/docs/walkthrough/problem.rst index 19758c83..4b28eaba 100644 --- a/docs/walkthrough/problem.rst +++ b/docs/walkthrough/problem.rst @@ -28,9 +28,9 @@ __ https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-co - name: main resources: requests: - ephemeral-storage: 10G + ephemeral-storage: 1G limits: - ephemeral-storage: 10G + ephemeral-storage: 1G There is a `PersistentVolumeClaim`__ resource kind, but it is persistent, i.e. not deleted after they are created (only manually deletable). @@ -74,3 +74,11 @@ The lifecycle of an ``EphemeralVolumeClaim`` is this: * Deletes the ``PersistentVolumeClaim`` after either the pod is finished, or the wait time has elapsed. + +.. seealso:: + This documentation only highlights the main patterns & tricks of Kopf, + but does not dive deep into the implementation of the operator's domain. + The fully functional solution for ``EphemeralVolumeClaim`` resources, + which is used for this documentation, is available at the following link: + + * https://github.com/nolar/ephemeral-volume-claims diff --git a/docs/walkthrough/resources.rst b/docs/walkthrough/resources.rst index 6380e659..5d4aa74a 100644 --- a/docs/walkthrough/resources.rst +++ b/docs/walkthrough/resources.rst @@ -8,8 +8,8 @@ Custom Resource Definition Let us define a CRD (custom resource definition) for our object. .. code-block:: yaml - :caption: crd.yaml - :name: crd-yaml + :caption: crd.yaml + :name: crd-yaml apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition @@ -57,8 +57,8 @@ logic behind the objects yet. Let's make a sample object: .. code-block:: yaml - :caption: obj.yaml - :name: obj-yaml + :caption: obj.yaml + :name: obj-yaml apiVersion: zalando.org/v1 kind: EphemeralVolumeClaim diff --git a/docs/walkthrough/starting.rst b/docs/walkthrough/starting.rst index e91d8d46..277fae7c 100644 --- a/docs/walkthrough/starting.rst +++ b/docs/walkthrough/starting.rst @@ -11,9 +11,9 @@ Let's start with the an operator skeleton that does nothing useful -- just to see how it can be started. .. code-block:: python - :name: skeleton - :linenos: - :caption: ephemeral.py + :name: skeleton + :linenos: + :caption: ephemeral.py import kopf @@ -37,9 +37,20 @@ The output looks like this: .. code-block:: none - ... TODO... - -.. todo:: add the outpit sample^^ + [2019-05-31 10:42:11,870] kopf.config [DEBUG ] configured via kubeconfig file + [2019-05-31 10:42:11,913] kopf.reactor.peering [WARNING ] Default peering object not found, falling back to the standalone mode. + [2019-05-31 10:42:12,037] kopf.reactor.handlin [DEBUG ] [default/my-claim] First appearance: {'apiVersion': 'zalando.org/v1', 'kind': 'EphemeralVolumeClaim', 'metadata': {'annotations': {'kubectl.kubernetes.io/last-applied-configuration': '{"apiVersion":"zalando.org/v1","kind":"EphemeralVolumeClaim","metadata":{"annotations":{},"name":"my-claim","namespace":"default"}}\n'}, 'creationTimestamp': '2019-05-29T00:41:57Z', 'generation': 1, 'name': 'my-claim', 'namespace': 'default', 'resourceVersion': '47720', 'selfLink': '/apis/zalando.org/v1/namespaces/default/ephemeralvolumeclaims/my-claim', 'uid': '904c2b9b-81aa-11e9-a202-a6e6b278a294'}} + [2019-05-31 10:42:12,038] kopf.reactor.handlin [DEBUG ] [default/my-claim] Adding the finalizer, thus preventing the actual deletion. + [2019-05-31 10:42:12,038] kopf.reactor.handlin [DEBUG ] [default/my-claim] Patching with: {'metadata': {'finalizers': ['KopfFinalizerMarker']}} + [2019-05-31 10:42:12,165] kopf.reactor.handlin [DEBUG ] [default/my-claim] Creation event: {'apiVersion': 'zalando.org/v1', 'kind': 'EphemeralVolumeClaim', 'metadata': {'annotations': {'kubectl.kubernetes.io/last-applied-configuration': '{"apiVersion":"zalando.org/v1","kind":"EphemeralVolumeClaim","metadata":{"annotations":{},"name":"my-claim","namespace":"default"}}\n'}, 'creationTimestamp': '2019-05-29T00:41:57Z', 'finalizers': ['KopfFinalizerMarker'], 'generation': 1, 'name': 'my-claim', 'namespace': 'default', 'resourceVersion': '47732', 'selfLink': '/apis/zalando.org/v1/namespaces/default/ephemeralvolumeclaims/my-claim', 'uid': '904c2b9b-81aa-11e9-a202-a6e6b278a294'}} + A handler is called with body: {'apiVersion': 'zalando.org/v1', 'kind': 'EphemeralVolumeClaim', 'metadata': {'annotations': {'kubectl.kubernetes.io/last-applied-configuration': '{"apiVersion":"zalando.org/v1","kind":"EphemeralVolumeClaim","metadata":{"annotations":{},"name":"my-claim","namespace":"default"}}\n'}, 'creationTimestamp': '2019-05-29T00:41:57Z', 'finalizers': ['KopfFinalizerMarker'], 'generation': 1, 'name': 'my-claim', 'namespace': 'default', 'resourceVersion': '47732', 'selfLink': '/apis/zalando.org/v1/namespaces/default/ephemeralvolumeclaims/my-claim', 'uid': '904c2b9b-81aa-11e9-a202-a6e6b278a294'}, 'spec': {}, 'status': {}} + [2019-05-31 10:42:12,168] kopf.reactor.handlin [DEBUG ] [default/my-claim] Invoking handler 'create_fn'. + [2019-05-31 10:42:12,173] kopf.reactor.handlin [INFO ] [default/my-claim] Handler 'create_fn' succeeded. + [2019-05-31 10:42:12,210] kopf.reactor.handlin [INFO ] [default/my-claim] All handlers succeeded for creation. + [2019-05-31 10:42:12,223] kopf.reactor.handlin [DEBUG ] [default/my-claim] Patching with: {'status': {'kopf': {'progress': None}}, 'metadata': {'annotations': {'kopf.zalando.org/last-handled-configuration': '{"apiVersion": "zalando.org/v1", "kind": "EphemeralVolumeClaim", "metadata": {"name": "my-claim", "namespace": "default"}, "spec": {}}'}}} + [2019-05-31 10:42:12,342] kopf.reactor.handlin [DEBUG ] [default/my-claim] Update event: {'apiVersion': 'zalando.org/v1', 'kind': 'EphemeralVolumeClaim', 'metadata': {'annotations': {'kopf.zalando.org/last-handled-configuration': '{"apiVersion": "zalando.org/v1", "kind": "EphemeralVolumeClaim", "metadata": {"name": "my-claim", "namespace": "default"}, "spec": {}}', 'kubectl.kubernetes.io/last-applied-configuration': '{"apiVersion":"zalando.org/v1","kind":"EphemeralVolumeClaim","metadata":{"annotations":{},"name":"my-claim","namespace":"default"}}\n'}, 'creationTimestamp': '2019-05-29T00:41:57Z', 'finalizers': ['KopfFinalizerMarker'], 'generation': 2, 'name': 'my-claim', 'namespace': 'default', 'resourceVersion': '47735', 'selfLink': '/apis/zalando.org/v1/namespaces/default/ephemeralvolumeclaims/my-claim', 'uid': '904c2b9b-81aa-11e9-a202-a6e6b278a294'}, 'status': {'kopf': {}}} + [2019-05-31 10:42:12,343] kopf.reactor.handlin [INFO ] [default/my-claim] All handlers succeeded for update. + [2019-05-31 10:42:12,362] kopf.reactor.handlin [DEBUG ] [default/my-claim] Patching with: {'status': {'kopf': {'progress': None}}, 'metadata': {'annotations': {'kopf.zalando.org/last-handled-configuration': '{"apiVersion": "zalando.org/v1", "kind": "EphemeralVolumeClaim", "metadata": {"name": "my-claim", "namespace": "default"}, "spec": {}}'}}} Note that the operator has noticed an object created before the operator was even started, and handled it -- since it was not handled before. diff --git a/docs/walkthrough/updates.rst b/docs/walkthrough/updates.rst index cba6ea9f..103e1636 100644 --- a/docs/walkthrough/updates.rst +++ b/docs/walkthrough/updates.rst @@ -2,23 +2,18 @@ Updating the objects ==================== +.. warning:: + Unfortunately, Minikube cannot handle the PVC/PV resizing, + as it uses the HostPath provider internally. + You can either skip this step of the tutorial, + or you can use an external Kubernetes cluster + with real dynamically sized volumes. + Previously (:doc:`creation`), we have implemented a handler for the creation of an ``EphemeralVolumeClaim`` (EVC), and created the corresponding ``PersistantVolumeClaim`` (PVC). What will happen if we change the size of the EVC when it already exists? -E.g., with: - -.. code-block:: bash - - kubectl edit evc my-claim - -Or by patching it: - -.. code-block:: bash - - kubectl patch evc my-claim -p '{"spec": {"resources": {"requests": {"storage": "100G"}}}}' - The PVC must be updated accordingly to match its parent EVC. First, we have to remember the name of the created PVC: @@ -28,17 +23,17 @@ with one additional line: .. code-block:: python :linenos: :caption: ephemeral.py - :emphasize-lines: 23 + :emphasize-lines: 24 @kopf.on.create('zalando.org', 'v1', 'ephemeralvolumeclaims') - def create_fn(spec, meta, namespace, logger, **kwargs): + def create_fn(body, spec, meta, namespace, logger, **kwargs): name = meta.get('name') size = spec.get('size') if not size: raise kopf.PermanentError(f"Size must be set. Got {size!r}.") - path = os.path.join(os.path.dirname(__file__), 'pvc-tpl.yaml') + path = os.path.join(os.path.dirname(__file__), 'pvc.yaml') tmpl = open(path, 'rt').read() text = tmpl.format(size=size, name=name) data = yaml.safe_load(text) @@ -61,11 +56,16 @@ We can see that with kubectl: .. code-block:: bash - kubectl describe evc my-claim + kubectl get -o yaml evc my-claim -.. code-block:: none +.. code-block:: yaml - TODO + spec: + size: 1G + status: + create_fn: + pvc-name: my-claim + kopf: {} Let's add a yet another handler, but for the "update" cause. This handler gets this stored PVC name from the creation handler, @@ -74,11 +74,11 @@ and patches the PVC with the new size from the EVC:: @kopf.on.update('zalando.org', 'v1', 'ephemeralvolumeclaims') def update_fn(spec, status, namespace, logger, **kwargs): - size = spec.get('create_fn', {}).get('size', None) + size = spec.get('size', None) if not size: raise kopf.PermanentError(f"Size must be set. Got {size!r}.") - pvc_name = status['pvc-name'] + pvc_name = status['create_fn']['pvc-name'] pvc_patch = {'spec': {'resources': {'requests': {'storage': size}}}} api = kubernetes.client.CoreV1Api() @@ -89,3 +89,36 @@ and patches the PVC with the new size from the EVC:: ) logger.info(f"PVC child is updated: %s", obj) + +Now, let's change the EVC's size: + +.. code-block:: bash + + kubectl edit evc my-claim + +Or by patching it: + +.. code-block:: bash + + kubectl patch evc my-claim --type merge -p '{"spec": {"size": "2G"}}' + +Keep in mind the PVC size can only be increased, never decreased. + +Give the operator few seconds to handle the change. + +Check the size of the actual PV behind the PVC, which is now increased: + +.. code-block:: bash + + kubectl get pv + +.. code-block:: none + + NAME CAPACITY ACCESS MODES ... + pvc-a37b65bd-8384-11e9-b857-42010a800265 2Gi RWO ... + +.. warning:: + Kubernetes & ``kubectl`` improperly show the capacity of PVCs: + it remains the same (1G) event after the change. + The size of actual PV (Persistent Volume) of each PVC is important! + This issue is not related to Kopf, so we go around it.