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