Kubernetes: Why does patching a custom resource's /status subresource update the parent resource?

8.6k views Asked by At

I'm trying to understand Kubernetes behaviour as it pertains to custom resources and their subresources -- specifically the status subresource.

Specifically, I want to update the status subresource without modifying the parent custom resource.

To the best of my understanding, this should be possible. I've reviewed the documentation [here][1], but I cannot seem to get it to work as anticipated.

I am testing using Docker Desktop's Kubernetes 1.19.3.

Here is the scenario ...

  1. Create this simple CRD:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: greetings.k8s.test.io
spec:
  group: k8s.test.io
  versions:
    - name: v1alpha1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              description: Greeting is the Schema for the Greetings Operator
              type: object
              properties:
                message:
                  description: A friendly greeting
                  type: string
                  default: Hello World!
            status:
              type: object
              properties:
                ready:
                  description: The resource's readiness
                  type: boolean
      additionalPrinterColumns:
      - name: ready
        type: boolean
        description: Readiness of the created resource
        jsonPath: .status.ready
      subresources:
        status: {}
  scope: Namespaced
  names:
    plural: greetings
    singular: greeting
    kind: Greeting
    shortNames:
    - grt
  1. Create a demo resource:
apiVersion: k8s.test.io/v1alpha1
kind: Greeting
metadata:
  name: demo
spec:
  message: Hi there!
  1. Fire up a proxy:
kubectl proxy &
  1. Establish a watch on custom resource:
curl -L -s -X GET -H "Content-Type: application/json" \
-H "Accept: application/json, */*" \
127.0.0.1:8001/apis//k8s.test.io/v1alpha1/watch/namespaces/default/greetings

At this point, you should see output similar the following:

{"type":"ADDED","object":{"apiVersion":"k8s.test.io/v1alpha1","kind":"Greeting","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"k8s.test.io/v1alpha1\",\"kind\":\"Greeting\",\"metadata\":{\"annotations\":{},\"name\":\"demo\",\"namespace\":\"default\"},\"spec\":{\"message\":\"Hi there!\"}}\n"},"creationTimestamp":"2020-12-10T04:02:55Z","generation":1,"managedFields":[{"apiVersion":"k8s.test.io/v1alpha1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}}},"f:spec":{".":{},"f:message":{}}},"manager":"kubectl-client-side-apply","operation":"Update","time":"2020-12-10T04:02:55Z"}],"name":"demo","namespace":"default","resourceVersion":"532930","selfLink":"/apis/k8s.test.io/v1alpha1/namespaces/default/greetings/demo","uid":"40f3a618-74e5-4b14-9bd4-2eb47366d804"},"spec":{"message":"Hi there!"}}}
  1. PATCH the /status subresource
curl -k -s -X PATCH -H "Accept: application/json, */*" \
-H "Content-Type: application/merge-patch+json" \
127.0.0.1:8001/apis/k8s.test.io/v1alpha1/namespaces/default/greetings/demo/status \
--data '{"status":{"ready":true}}'

Confusingly, after submitting the PATCH our watch on the parent resource produces ...

{"type":"MODIFIED","object":{"apiVersion":"k8s.test.io/v1alpha1","kind":"Greeting","metadata":{"annotations":{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"k8s.test.io/v1alpha1\",\"kind\":\"Greeting\",\"metadata\":{\"annotations\":{},\"name\":\"demo\",\"namespace\":\"default\"},\"spec\":{\"message\":\"Hi there!\"}}\n"},"creationTimestamp":"2020-12-10T04:02:55Z","generation":1,"managedFields":[{"apiVersion":"k8s.test.io/v1alpha1","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:kubectl.kubernetes.io/last-applied-configuration":{}}},"f:spec":{".":{},"f:message":{}}},"manager":"kubectl-client-side-apply","operation":"Update","time":"2020-12-10T04:02:55Z"},{"apiVersion":"k8s.test.io/v1alpha1","fieldsType":"FieldsV1","fieldsV1":{"f:status":{".":{},"f:ready":{}}},"manager":"curl","operation":"Update","time":"2020-12-10T04:05:22Z"}],"name":"demo","namespace":"default","resourceVersion":"533184","selfLink":"/apis/k8s.test.io/v1alpha1/namespaces/default/greetings/demo","uid":"40f3a618-74e5-4b14-9bd4-2eb47366d804"},"spec":{"message":"Hi there!"},"status":{"ready":true}}}

Why is it MODIFYING the parent resource??

And if we inspect the resource, we can see that that status of the subresource was definitely updated:

$ k get grt demo
NAME   READY
demo   true

I don't know if this behaviour is expected and my understanding is faulty OR if my PATCH methodology is flawed.

Hope somebody can help out. Thanks.

UPDATE:

In addition to PATCH I can also confirm that PUT (while a more complicated operation) exhibits the exact same behaviour.

Example:

curl -k -s -X PUT -H "Accept: application/json, */*" \
-H "Content-Type: application/json" \
127.0.0.1:8001/apis/k8s.test.io/v1alpha1/namespaces/default/greetings/demo/status \
--data '{"apiVersion":"k8s.test.io/v1alpha1","kind":"Greeting","metadata":{"name":"demo","resourceVersion":"533184"},"status":{"ready":false}}'

¯\(ツ)

1

There are 1 answers

2
David Maze On

The status subresource isn't actually a different object; it is just a separate API path that can only modify the top-level status: block in the object. This is useful because you can set up an RBAC policy for your controller to allow read access to the whole object, but only allow writing the object's status.

In particular, you see this in your last command. If you kubectl get grt demo -o yaml, the extended YAML syntax will include the status: sub-block. The controller updating the status will cause a visible change in the status:, so you should reasonably expect that a watch will see this as a change.