Skip to main content
Version: v0.11.x

Patching Syntax

This guide explains how to use the patching system in OpenChoreo Traits to modify resources generated by ComponentTypes.

Overview​

Traits can modify existing resources using patches, which are JSON Patch operations enhanced with:

  • Array filtering using JSONPath-like syntax
  • CEL-based resource targeting
  • forEach iteration support

Basic Patch Structure​

Patches are defined in the Trait's spec.patches section:

apiVersion: openchoreo.dev/v1alpha1
kind: Trait
metadata:
name: monitoring-sidecar
spec:
patches:
- target:
kind: Deployment
group: apps
version: v1
operations:
- op: add
path: /spec/template/spec/containers/-
value:
name: prometheus-exporter
image: prom/node-exporter:latest
ports:
- containerPort: 9100

Supported Operations​

add​

Adds a value at the specified path:

# Add a new label
- op: add
path: /metadata/labels/monitoring
value: "enabled"

# Append to array (using -)
- op: add
path: /spec/containers/-
value:
name: sidecar
image: sidecar:latest

# Add nested structure
- op: add
path: /metadata/annotations/example.com~1version
value: "v2.0"

replace​

Replaces an existing value. The path must exist:

# Replace a value
- op: replace
path: /spec/replicas
value: 3

# Replace array element
- op: replace
path: /spec/containers/0/image
value: nginx:latest

remove​

Removes a value at the path:

# Remove a label
- op: remove
path: /metadata/labels/deprecated

# Remove array element
- op: remove
path: /spec/containers/1

Array Filtering​

Use JSONPath-like syntax to target specific array elements by field value:

Basic Filtering​

# Target container by name
- op: add
path: /spec/template/spec/containers[?(@.name=='app')]/env/-
value:
name: MONITORING
value: enabled

# Target volume by name
- op: replace
path: /spec/template/spec/volumes[?(@.name=='data')]/emptyDir
value:
sizeLimit: 10Gi

Nested Field Filters​

# Filter by nested field path (dot notation)
- op: replace
path: /spec/containers[?(@.resources.limits.memory=='2Gi')]/image
value: app:high-mem-v2

# Filter by configMap name
- op: add
path: /spec/volumes[?(@.configMap.name=='app-config')]/configMap/defaultMode
value: 0644

Filter Limitations​

Supported: Simple equality filters of the form @.field.path=='value'

# Supported
- op: add
path: /spec/containers[?(@.name=='app')]/env/-
value: {name: VAR, value: val}

Not supported: Multiple conditions (&&, ||), operators like contains, array indexing in filters, or existence checks.

CEL Expression Support​

CEL expressions (${...}) can be used in patch fields:

In path​

# Dynamic path segments
- op: add
path: /data/${env.name}
value: ${env.value}

# Dynamic filter conditions
- op: add
path: /spec/containers[?(@.name=='${parameters.containerName}')]/env/-
value:
name: VERSION
value: ${parameters.version}

In value​

- op: add
path: /metadata/labels/app
value: ${metadata.name}

- op: add
path: /spec/template/spec/containers/-
value:
name: ${parameters.sidecarName}
image: ${parameters.sidecarImage}
ports:
- containerPort: ${parameters.sidecarPort}

Resource Targeting​

Basic Targeting​

The target spec requires kind, group, and version:

patches:
# Patch apps/v1 Deployment
- target:
kind: Deployment
group: apps
version: v1
operations:
- op: add
path: /metadata/labels/patched
value: "true"

# Patch core v1 Service (empty string for core API)
- target:
kind: Service
group: "" # Empty string for core API resources
version: v1
operations:
- op: add
path: /metadata/annotations/patched
value: "true"

Key points:

  • kind, group, and version are required
  • For core API resources (Service, ConfigMap, Secret), use group: ""
  • targetPlane is optional, defaults to "dataplane"

CEL-Based Filtering with where​

Use where clause to target resources conditionally:

patches:
- target:
kind: Deployment
group: apps
version: v1
where: ${resource.spec.replicas > 1} # Only multi-replica deployments
operations:
- op: add
path: /metadata/annotations/ha-mode
value: "true"

- target:
kind: Service
group: ""
version: v1
where: ${resource.spec.type == 'LoadBalancer'}
operations:
- op: add
path: /metadata/annotations/external
value: "true"

ForEach Iteration​

Apply patches iteratively over a list:

patches:
- target:
kind: ConfigMap
group: ""
version: v1
forEach: ${parameters.environments}
var: env
operations:
- op: add
path: /data/${env.name}
value: ${env.value}

- target:
kind: Deployment
group: apps
version: v1
forEach: ${parameters.extraPorts}
var: port
operations:
- op: add
path: /spec/template/spec/containers[?(@.name=='app')]/ports/-
value:
containerPort: ${port.number}
name: ${port.name}

Path Resolution Behavior​

Path TypeOperationBehavior
Map keyaddAuto-creates parent maps if missing
Map keyreplaceError if target doesn't exist
Map keyremoveIdempotent - no error if key doesn't exist
Filter [?(...)]add, replace, removeError if no match
Array indexadd, replace, removeError if index out of bounds

Auto-create and idempotent removal - The add operation automatically creates missing parent objects, and remove on map keys succeeds silently if the key doesn't exist. This reduces boilerplate and matches Kubernetes Strategic Merge Patch behavior.

Array indices - All array index operations error on out-of-bounds indices, as this likely indicates a mismatch between the patch and the resource. Use filters like [?(@.name=='app')] instead of positional indices for resilient patches.

Path Escaping​

Paths use JSON Pointer syntax with special character escaping:

  • / in a key β†’ ~1
  • ~ in a key β†’ ~0

This is commonly needed for Kubernetes annotations that contain /:

# Annotation: kubernetes.io/ingress-class
- op: add
path: /metadata/annotations/kubernetes.io~1ingress-class
value: nginx

# Annotation: sidecar.istio.io/inject
- op: add
path: /spec/template/metadata/annotations/sidecar.istio.io~1inject
value: "true"

# Key containing ~
- op: add
path: /data/config~0backup
value: backup-data

Common Patterns​

Safe Label Addition​

# Good - adds individual labels without affecting existing ones
- op: add
path: /metadata/labels/monitoring
value: enabled

# Bad - replaces all existing labels
- op: replace
path: /metadata/labels
value:
monitoring: enabled

Add Sidecar Container​

patches:
- target:
kind: Deployment
group: apps
version: v1
operations:
# Add sidecar container
- op: add
path: /spec/template/spec/containers/-
value:
name: fluentd
image: fluent/fluentd:v1.14
volumeMounts:
- name: logs
mountPath: /var/log

# Add shared volume
- op: add
path: /spec/template/spec/volumes/-
value:
name: logs
emptyDir: {}

# Mount to main container
- op: add
path: /spec/template/spec/containers[?(@.name=='app')]/volumeMounts/-
value:
name: logs
mountPath: /app/logs

Volume Mount Injection​

- op: add
path: /spec/template/spec/volumes/-
value:
name: config
configMap:
name: ${metadata.name}-config

- op: add
path: /spec/template/spec/containers[?(@.name=='app')]/volumeMounts/-
value:
name: config
mountPath: /etc/config
readOnly: true

Environment Variables from Parameters​

patches:
- target:
kind: Deployment
group: apps
version: v1
forEach: ${parameters.envVars}
var: envVar
operations:
- op: add
path: /spec/template/spec/containers[?(@.name=='${parameters.containerName}')]/env/-
value:
name: ${envVar.name}
value: ${envVar.value}
tip

Use filters for explicit targeting - Prefer [?(@.name=='app')] over positional indices like [0]. Filters are more resilient to changes in resource structure.