Blog articles

Code and the Kubernetes JSON Object

May 10, 2023

Kubernetes objects are persistent entities that represent the state of our cluster. They describe what we have deployed or want to deploy, such as a simple pod, a network configuration, or an application deployment with all its mappings, resources, and policies. The Kubernetes documentation uses the term a “record of intent” — but what about when we want to validate, change, or otherwise work with this record before Kubernetes deploys it? 

For this, we can code our own dynamic admission controllers that validate or manipulate our object. For this, we won’t be working with the human-friendly Kubernetes request objects we’re so used to; instead, we’ll be seeing our objects as Kubernetes see them: As JSON objects.

Taking a look at under the hood

Let’s take a look at a simple pod request, written as we would write it:

apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
spec:
  containers:
  - name: nginx
	image: docker.io/nginx:latest

This isn’t what Kubernetes sees, however. When Kubernetes actually works with the object, it looks like this:

{
  kind: 'AdmissionReview',
  apiVersion: 'admission.k8s.io/v1',
  request: {
	uid: 'c83425ea-084c-47ae-939c-af0f34c038dd',
	kind: { group: '', version: 'v1', kind: 'Pod' },
	resource: { group: '', version: 'v1', resource: 'pods' },
	requestKind: { group: '', version: 'v1', kind: 'Pod' },
	requestResource: { group: '', version: 'v1', resource: 'pods' },
	name: 'nginx-pod',
	namespace: 'default',
	operation: 'CREATE',
	userInfo: {
  	username: 'kubernetes-admin',
  	uid: 'aws-iam-authenticator:187383671122:AIDASXIHP5FJDYSGIGR7L',
  	groups: [Array],
  	extra: [Object]
	},
	object: {
  	kind: 'Pod',
  	apiVersion: 'v1',
  	metadata: [Object],
  	spec: [Object],
  	status: {}
	},
	oldObject: null,
	dryRun: false,
	options: {
  	kind: 'CreateOptions',
  	apiVersion: 'meta.k8s.io/v1',
  	fieldManager: 'kubectl-client-side-apply',
  	fieldValidation: 'Strict'
	}
  }
}

And this is the object we need to work with in our code, retrieved via console log with the reference request named body. You’ll notice how some of the JSON data is condensed, however—see how properties such as metadata and spec contain the value [object]. This isn’t the literal text “[object],” but instead more JSON data that was filtered out in our console log. 

That doesn’t mean we can’t access that data, though—we absolutely can! Instead of calling our overall body object to achieve this, we can reference each sub-parameter as body.<param>, such as body.request.object.spec or body.request.object.metadata—easily two of our most important objects, if working with altering or validating our deployments.

Working With Objects

body.request.object.metadata contains our metadata objects, such as labels and names. Generally, this is what we’ll work for is validating or altering our labels and tagging. When expanded, it looks like this:

{
  name: 'tester',
  namespace: 'default',
  creationTimestamp: null,
  annotations: {
	'kubectl.kubernetes.io/last-applied-configuration': '{"apiVersion":"v1","kind":"Pod","metadata":{"annotations":{},"name":"tester","namespace":"default"},"spec":{"containers":[{"image":"docker.io/nginx:latest","name":"nginx"}]}}\n'
  },
  managedFields: [
	{
  	manager: 'kubectl-client-side-apply',
  	operation: 'Update',
  	apiVersion: 'v1',
  	time: '2023-04-17T16:41:36Z',
  	fieldsType: 'FieldsV1',
  	fieldsV1: [Object]
	}
  ]
}

If we are creating a mutating webhook that adds labels — missing from above because we add none in our initial pod description —it is here we would want to inject the data. We do it in this JSON format, not as though it were a YAML object, the way we would when writing it (We would also need the object to be Base64 encoded, but that’s off topic). 

For example, here’s some Node.js code that does just that — adds a label to the metadata — in its JSON glory (and not encoded, so it wouldn’t work but we can see it):

  res.send({
  	apiVersion: 'admission.k8s.io/v1',
  	kind: 'AdmissionReview',
  	response: {
      uid: uid,
      allowed: true,
      patchType: "JSONPatch",
      patch: "[{"op": "add", "path": "/metadata/labels", "value": {"env": "dev"}}]",
  	},
  });

Of course, we won’t always be altering our data, and often we may want to work with more than one parameter, particularly when working within body.request.object.spec, which contains our deployment information, including things like our container information when launching pods. For this we’ll want to consider how we parse our data. 

Let’s consider the body.request.object.spec for our example pod deployment:

{
  volumes: [ { name: 'kube-api-access-9fs5n', projected: [Object] } ],
  containers: [
	{
  	name: 'nginx',
  	image: 'docker.io/nginx:latest',
  	resources: {},
  	volumeMounts: [Array],
  	terminationMessagePath: '/dev/termination-log',
  	terminationMessagePolicy: 'File',
  	imagePullPolicy: 'Always'
	}
  ],
  restartPolicy: 'Always',
  terminationGracePeriodSeconds: 30,
  dnsPolicy: 'ClusterFirst',
  serviceAccountName: 'default',
  serviceAccount: 'default',
  securityContext: {},
  schedulerName: 'default-scheduler',
  tolerations: [
	{
  	key: 'node.kubernetes.io/not-ready',
  	operator: 'Exists',
  	effect: 'NoExecute',
  	tolerationSeconds: 300
	},
	{
  	key: 'node.kubernetes.io/unreachable',
  	operator: 'Exists',
  	effect: 'NoExecute',
  	tolerationSeconds: 300
	}
  ],
  priority: 0,
  enableServiceLinks: true,
  preemptionPolicy: 'PreemptLowerPriority'
}

If we wanted to access multiple values from the containers parameter, we might want to consider writing our code so that we can iterate through the array of data and reference each as, for example, container.name or container.image instead of the longer body.request.object.spec.containers.name or body.request.object.spec.containers.image. Consider this Node.JS example, wherein we call parameters from the body object four different times in different ways:

  const uid = req.body.request.uid;
  const object = req.body.request.object;

  for (var container of object.spec.containers) {
	if (container.image.endsWith('latest')) {
...<code truncated>...      
	}
	else {
     ...<code truncated>... 
	}
  }

The first two objects—body.request.uid and body.request.object—call the object item they need directly; in this case, the UID of the request and then the entire request.object, which we can now call by the shorthand object. Reassignments such as these make working with our overall object in our code much simpler. 

We see a further example of this is the for loop, which further iterates through our object.spec.containers (really body.request.object.spec.containers) so we can do things like reference our body.request.obect.spec.containers.image as a simple containers.image while in our loop.

Conclusion

Getting started working with Kubernetes objects within our code can be intimidating, especially if you’re used to the human-readable YAML approach with which Kubernetes generally works. Yet all our JSON objects are just that: JSON. The same information is provided in both files, with the JSON providing an expanded overview of what goes on when we run a kubectl apply against our test pod — or any pod, deployment, etc.

To see this example code (and some others) in action, be sure to check out the course it was taken from, Designing Dynamic Kubernetes Admission Controllers, to get started extending your Kubernetes architecture.


Elle Krout

With a background at a cloud hosting company, Elle learning Linux and DevOps from the bottom up as a technical writer set out to document configuration management systems. Falling in love with Linux, DevOps and tech as a whole, she eventually took her ability to explain technical concepts to wide audiences to the training area, creating courses on Red Hat, SaltStack, HashiCorp, LXD, Docker, and more.