Kubernetes: targeting the addition of array items to a multi-document yaml manifest

If you have a Kubernetes yaml manifest that contains multiple documents, targeting a single document for modification while still outputting the other documents untouched can be a challenge.

As an example, consider the simple example below were you have a single yaml file that contains: a Namespace, Deployment, and DaemonSet.  And we want to add a row to the template annotations of only the DaemonSet.

# multi-doc.yaml
---
kind: namespace
metadata:
  name: mynamespace
---
kind: DaemonSet
metadata:
  name: mydaemonset
spec:
  template:
    metadata:
      annotations:
        my/annotation: "is-daemonset"
        # we want to add an annotation here
---
kind: Deployment
metadata:
  name: mydeployment
spec:
  template:
    metadata:
      annotations:
        my/annotation: "is-deployment"

We could (post-deployment) do a “kubectl annotation” directly on the pods created, but that would be a one-time modification that would not persist through pod recreations.  We could also (post-deployment) do a ‘kubectl patch’ of the DaemonSet as described in my article here, but that would require an additional step.

Instead, let’s use the yq utility to make the manifest modification to multi-doc.yaml, which can then be applied to the Kubernetes cluster.

Jump to the end of this article if you want to skip the discussion and instead view the final solution.

Installing yq utility

For these yaml manipulations, we will be using the ‘yq‘ 4.18+ utility.  On Ubuntu, it can be installed using:

sudo snap install yq

Or the latest ‘yq‘ for linux can also be installed manually:

# get latest version
latest_yq_linux=$(curl -sL https://api.github.com/repos/mikefarah/yq/releases/latest | jq -r ".assets[].browser_download_url" | grep linux_amd64.tar.gz)

# download and make available to PATH
wget $latest_yq_linux
tar xvfz yq_linux_amd64.tar.gz
sudo cp yq_linux_amd64 /usr/local/bin/yq
sudo chown root:root /usr/local/bin/yq

You will need yq 4.18+ for the syntax in this article to work.

yq --version

First attempt at yq logic (not correct)

If you wanted to add an annotation to the Daemonset to enable Prometheus scraping, your first try might be:

# not what we want! adds annotation indiscriminately to every document
$ cat multi-doc.yaml | yq '.spec.template.metadata.annotations."prometheus.io/scrape"="true"'

---
kind: namespace
metadata:
  name: mynamespace
spec:
  template:
    metadata:
      annotations:
        prometheus.io/scrape: "true"
---
kind: DaemonSet
metadata:
  name: mydaemonset
spec:
  template:
    metadata:
      annotations:
        my/annotation: "is-daemonset"
        prometheus.io/scrape: "true"
---
kind: Deployment
metadata:
  name: mydeployment
spec:
  template:
    metadata:
      annotations:
        my/annotation: "is-deployment"
        prometheus.io/scrape: "true"

But notice this adds an annotation to every document, which is not what we want.  It even adds the spec.template.metadata.annotations element to the Namespace document.

Second attempt at yq logic (not correct)

In order to target just the document we want, let’s add a select filter.

$ cat multi-doc.yaml | yq 'select(.kind=="DaemonSet") | .spec.template.metadata.annotations."prometheus.io/scrape"="true"'

kind: DaemonSet
metadata:
  name: mydaemonset
spec:
  template:
    metadata:
      annotations:
        my/annotation: "is-daemonset"
        prometheus.io/scrape: "true"

The custom annotation is added to just the DaemonSet correctly.  But now the problem is that only the single document is output, the Namespace and Deployment documents are filtered out.

We would have a similar issue if we targeted the .spec.template.metadata.annotations because both Deployment+DaemonSet would be modified, while Namespace would be filtered out.

We could reverse the filter (change == to !=) to get the inverse set of documents, but what we really want is a full evaluation in a single pass which is described in the next section.

Correct logic for targeting single document, but getting all documents output

In order to get what we really want, which is modification of select targeted documents, but to output all documents, we need conditional logic.

yq does not offer if/elif/else like jq, but it does provide ‘select’ and ‘with’ that can serve as conditionals.

$ cat multi-doc.yaml | yq 'select (.spec.template.metadata.annotations) |= (  
  select (.kind=="DaemonSet") | 
  with(
    select(.spec.template.metadata.annotations."prometheus.io/scrape"==null); 
      .spec.template.metadata.annotations."prometheus.io/scrape"="true" | .spec.template.metadata.annotations."prometheus.io/port"="10254"
  )  
)'

---
kind: namespace
metadata:
  name: mynamespace
---
kind: DaemonSet
metadata:
  name: mydaemonset
spec:
  template:
    metadata:
      annotations:
        my/annotation: "is-daemonset"
        prometheus.io/scrape: "true"
        prometheus.io/port: "10254"
---
kind: Deployment
metadata:
  name: mydeployment
spec:
  template:
    metadata:
      annotations:
        my/annotation: "is-deployment"

We can see that the Namespace and Deployment are output without modification, while the DaemonSet now has the two additional custom annotations.

This can now be fed directly into ‘kubectl apply’ for deployment to the cluster.

 

REFERENCES

mikefarah yq docs, logic without if/else

yq, github project for manipulation of yaml

jq utility, manipulation of json

fabianlee github, bash script used in this article