Skip to main content
Version: v1.2.0-m.1 (pre-release)

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

A Trait can also delete whole resources produced by the ComponentType or earlier traits using the spec.removes section. See Removing Resources.

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}

Removing Resources

Patches modify resources in place. When a Trait needs to delete a whole resource produced by the ComponentType or by an earlier trait, use the spec.removes section instead. Each entry matches resources by GVK (with an optional where filter) and removes the matched resources entirely from the rendered output.

apiVersion: openchoreo.dev/v1alpha1
kind: Trait
metadata:
name: drop-default-route
spec:
removes:
- target:
kind: HTTPRoute
group: gateway.networking.k8s.io
version: v1
targetPlane: dataplane

A remove entry uses the same target shape as a patch, but has no operations. The whole matched resource is deleted:

FieldRequiredDescription
target.kindYesResource kind to remove (e.g. HTTPRoute, ConfigMap)
target.groupYesAPI group; use "" for core API resources
target.versionYesAPI version (e.g. v1)
target.whereNoCEL expression filtering which matching resources are removed
targetPlaneNoPlane whose resources are targeted; defaults to "dataplane"
forEach / varNoIterate over a CEL list, binding each item to var for use in where

Execution order - Within a single trait, removes run after its creates and patches. This lets one trait fully express a substitution: create a replacement resource and then remove the original.

spec:
creates:
- template:
apiVersion: v1
kind: ConfigMap
metadata:
name: ${metadata.name}-tuned-config
data:
mode: optimized
removes:
# Drop the ConfigMap the ComponentType emitted, now that the tuned one exists
- target:
kind: ConfigMap
group: ""
version: v1
where: ${resource.metadata.name == metadata.name + "-default-config"}
Workload resources cannot be removed

The primary workload is defined by the ComponentType, so traits must not delete it. The admission webhook rejects removes that target a built-in workload GVK: kinds Deployment, StatefulSet, DaemonSet, CronJob, or Job in the apps or batch groups. The match is on the full GVK, so a custom CRD that merely shares one of these kind names in a different group (e.g. group: example.com, kind: Deployment) is not rejected.

forEach is supported just like in patches, letting you remove a set of resources derived from a CEL list:

removes:
- target:
kind: HTTPRoute
group: gateway.networking.k8s.io
version: v1
where: ${resource.metadata.labels["openchoreo.dev/endpoint-name"] == route}
forEach: ${parameters.routesToDrop}
var: route

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.