I was recently struggling to add a single annotation to a Kubernetes object definition, using a JSON Patch with Kustomize, when I stumbled on the JSON Pointer syntax. It’s fleetingly referenced in the Kustomize documentation, and can be useful in some circumstances. So, here’s a summary of what it is, and what it does.

The Problem Link to heading

My challenge was to add an annotation to a single Kubernetes object with Kustomize. The commonAnnotations field adds annotations to all rendered objects, so that’s no good for the job at hand, and the builtin Annotations Transformer doesn’t cut it either. We could use a Strategic Merge patch instead, but where would the fun be in that?! A JSON Patch, targeting the specific object definition, seems the best way to go. Let’s suppose we have an object definition in the Kustomize base that contains the following:

apiVersion: gateway.networking.k8s.io
kind: Gateway
metadata:
  name: https-gw
  annotations:
    foo: bar

<snip>

A JSON Patch can be defined to add the annotation cert-manager.io/issuer: letsencrypt-issuer at the path (target location) in the JSON document that represents the object. We could then perform a Kustomize build, and apply the JSON Patch defined in a Kustomization:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patches:
  - patch: |-
      - op: add
        path: /metadata/annotations
        value:
          cert-manager.io/issuer: letsencrypt-issuer      
    target:
      apiVersion: gateway.networking.k8s.io
      kind: Gateway
      name: https-gw

We’d hope that this renders the following from the build:

apiVersion: gateway.networking.k8s.io
kind: Gateway
metadata:
  name: https-gw
  annotations:
    foo: bar
    cert-manager.io/issuer: letsencrypt-issuer

<snip>

Alas, not so. Instead, we get the following, with the original foo: bar annotation replaced with the annotation defined in the patch:

apiVersion: gateway.networking.k8s.io
kind: Gateway
metadata:
  name: https-gw
  annotations:
    cert-manager.io/issuer: letsencrypt-issuer

The reason for this is explained in the JSON Patch IETF RFC6902 standard for the ‘add’ operation:

If the target location specifies an object member that does exist, that member’s value is replaced.

Because the target location specified by the patch’s path (/metadata/annotations) already has a value in the Kustomize base definition (foo: bar), it gets clobbered with the value defined in the patch. That’s not what we want.

Potential Solution Link to heading

In the search for a solution, we could draw on another one of the IETF RFC6902 functions for the ‘add’ operation:

If the target location specifies an object member that does not already exist, a new member is added to the object.

The patch could be amended to include the annotation’s key in the path, which should get added to the object with the value specified:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patches:
  - patch: |-
      - op: add
        path: /metadata/annotations/cert-manager.io/issuer
        value: letsencrypt-issuer      
    target:
      apiVersion: gateway.networking.k8s.io
      kind: Gateway
      name: https-gw

But, this time, there is an error:

Error: add operation does not apply: doc is missing path: "/metadata/annotations/cert-manager.io/issuer": missing value

The proposed key for the annotation, cert-manager.io/issuer, has been interpreted as two different components of the path to the target location in the JSON document. In effect, we’re trying to add the ‘object member’ issuer to a target location that doesn’t exist.

The Solution Link to heading

The JSON Patch specification requires the value of the path to be a string containing a JSON Pointer; a sequence of zero or more reference tokens, each prefixed by a /. The characters / and ~ both have speical meanings in JSON Pointer. So, if one of the reference tokens contains a /, as in our annotation example, it needs to be escaped so that it can be interpreted literally. The sequence ~1 is used in place of / where it is to be interpreted literally. The JSON Patch needs to be re-defined, thus:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
patches:
  - patch: |-
      - op: add
        path: /metadata/annotations/cert-manager.io~1issuer
        value: letsencrypt-issuer      
    target:
      apiVersion: gateway.networking.k8s.io
      kind: Gateway
      name: https-gw

Now we’re adding the object member cert-manager.io/issuer to the object. Rendering this Kustomization with a Kustomize build, gives us just what we need:

apiVersion: gateway.networking.k8s.io
kind: Gateway
metadata:
  name: https-gw
  annotations:
    foo: bar
    cert-manager.io/issuer: letsencrypt-issuer

<snip>

Conclusion Link to heading

I’ve always viewed the JSON Patch mechanism in Kustomize as a bit of a black art. Sometimes it pays to go and read up on what you’re actually trying to do! RFCs may not be everyone’s cup of tea, so https://jsonpatch.com/ is a little easier on the eye.