Skip to main content

Traits

A Trait is a composable capability you attach to a component without changing its ComponentType. Where a ComponentType defines the base shape of a workload, a Trait is an overlay — persistent storage, autoscaling, a monitoring sidecar, network policy — that any compatible component can opt into. Authoring Traits is how you offer developers a menu of vetted add-ons they mix and match, instead of forking a ComponentType for every combination of features.

note

DevsPortal is built on the Control Plane Operator engine. Trait, ClusterTrait, the openchoreo.dev/v1alpha1 API group, and the occ CLI are unchanged engine internals. Examples target the Fedshi instance.

Portal first, CLI second

Author Traits from the portal's Create… templates — they open the in-portal YAML editor, pre-filled with the right kind. The occ/YAML form below is for scripting, review, and GitOps. Trait authoring is schema, patch, and CEL work, so there's no no-YAML wizard — the portal gives you the editor and validation.

Where you author them

In the portal: open Create… in the left nav and pick the ClusterTrait template (scroll the Platform Resources section to find it), or the namespace-scoped Trait template. It opens the in-portal YAML editor seeded with the kind, where you fill in the schema, creates, and patches described below.

Or with the occ CLI: write the manifest and apply it — see Managing them with the CLI below.

Why you author them

Two component types that both need "add a persistent volume" shouldn't each re-implement it. Traits let you define orthogonal concerns once and let developers compose them:

  • Reusable operational patterns — storage, autoscaling, monitoring, sidecars — defined in one place, attachable to many component types.
  • Composability without combinatorial explosion — rather than web-service, web-service-with-storage, web-service-with-storage-and-hpa, you ship one web-service ComponentType plus persistent-volume and autoscaler Traits.
  • Governed à-la-carte capability — a ComponentType's allowedTraits controls which Traits its components may attach, so developers get choice within boundaries you set.

A developer attaches only the capabilities they need, with instance-specific parameters, and can attach the same Trait multiple times (two volumes, say) using distinct instance names.

Trait vs ClusterTrait

A ClusterTrait is the cluster-scoped variant; same spec, different scope. The platform's shared cross-cutting capabilities ship as ClusterTraits so every organization namespace can use them. Author a namespace-scoped Trait only when an org needs its own capability or to override a default.

The anatomy of a Trait

A Trait is schema-driven like a ComponentType, plus two operations that distinguish it: it can create new resources and patch existing ones.

PartFieldWhat it does
Parameter schemaparameters.openAPIV3SchemaWhat a developer sets per trait instance (mount path, volume name)
Environment schemaenvironmentConfigs.openAPIV3SchemaWhat's overridable per environment via the binding (size, storage class)
CreatescreatesNew Kubernetes resources the Trait adds (PVC, ConfigMap, ServiceMonitor)
PatchespatchesModifications to resources the ComponentType already generated (add a volume mount, inject env, attach a sidecar)

The parameters / environmentConfigs split is identical to ComponentTypes: parameters are captured in the release and applied everywhere; environment configs are overridden per environment through the binding (keyed by the trait's instanceName). That's how a volume can be 10Gi in dev and 100Gi in prod from the same release.

Creates vs patches

This is the conceptual core of a Trait:

  • creates generate new resources that don't exist in the base ComponentType. A storage Trait creates a PersistentVolumeClaim; a monitoring Trait creates a ServiceMonitor.
  • patches modify resources the ComponentType already produced, using JSON-Patch operations (with array filtering). This is how a Trait reaches into the Deployment the ComponentType made and adds a volume, mounts it into the main container, injects an env var, or attaches a sidecar — without the ComponentType knowing the Trait exists.

Both creates templates and patches values support the same ${...} CEL interpolation as ComponentTypes, so a patch can reference the developer's parameters and the trait instance name.

A representative example

A persistent-volume ClusterTrait: it creates a PVC and patches the ComponentType's Deployment to mount it.

apiVersion: openchoreo.dev/v1alpha1
kind: ClusterTrait
metadata:
name: persistent-volume
spec:
# Set per trait instance in Component.spec.traits[].parameters
parameters:
openAPIV3Schema:
type: object
properties:
volumeName:
type: string
mountPath:
type: string

# Overridable per environment in ReleaseBinding.spec.traitEnvironmentConfigs
environmentConfigs:
openAPIV3Schema:
type: object
properties:
size:
type: string
default: "10Gi"
storageClass:
type: string
default: "standard"

# New resource this Trait adds
creates:
- template:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
# trait.instanceName makes the name unique per attachment
name: ${metadata.name}-${trait.instanceName}
namespace: ${metadata.namespace}
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ${environmentConfigs.storageClass}
resources:
requests:
storage: ${environmentConfigs.size}

# Modifications to the ComponentType's Deployment
patches:
- target:
kind: Deployment
group: apps
version: v1
operations:
- op: add
path: /spec/template/spec/volumes/-
value:
name: ${parameters.volumeName}
persistentVolumeClaim:
claimName: ${metadata.name}-${trait.instanceName}
- op: add
path: /spec/template/spec/containers[?(@.name=='main')]/volumeMounts/-
value:
name: ${parameters.volumeName}
mountPath: ${parameters.mountPath}

Two things to notice:

  • trait.instanceName is what makes the same Trait attachable multiple times. Because the PVC name and the patch both key off it, two persistent-volume instances produce two PVCs and two distinct mounts.
  • The array-filtered patch path containers[?(@.name=='main')]/volumeMounts/- finds the main container in whatever Deployment the ComponentType generated and appends to it. The Trait doesn't need to know how that Deployment was built — only the container name contract.

Allowing a Trait on a ComponentType

A Trait isn't usable until a ComponentType lists it in allowedTraits. That's the governance join between the two:

# in the ComponentType
spec:
allowedTraits:
- kind: ClusterTrait
name: persistent-volume
- kind: ClusterTrait
name: autoscaler

Developers of that component type can attach those Traits and no others — capability choice inside boundaries you control. See Component types → allowed traits.

How developers consume it

A developer attaches the Trait to a component, naming the instance and supplying parameters:

spec:
componentType:
kind: ClusterComponentType
name: deployment/web-service
traits:
- name: persistent-volume
kind: ClusterTrait
instanceName: data-storage # unique per attachment
parameters:
volumeName: data
mountPath: /var/data

Per-environment overrides land in the binding, keyed by instanceName:

# in the ReleaseBinding
spec:
traitEnvironmentConfigs:
data-storage:
size: "100Gi"
storageClass: "production-ssd"

So the developer attaches 10Gi standard storage by default, and you (or they, with the right access) size it up to 100Gi of SSD in production — same release, environment-specific config.

Managing them with the CLI

occ clustertrait list
occ clustertrait get persistent-volume
occ trait list -n fedshi
occ apply -f persistent-volume.yaml

Deep reference

The patching grammar (JSON-Patch operations, array filters), the schema features, and the full CEL surface are documented upstream:

What's next

  • Component types — the base templates Traits overlay and the allowedTraits that gate them.
  • Resource types — for managed infrastructure (databases, queues), which is a separate abstraction from a Trait.
  • Governance and guardrails — Traits as a vetted-capability guardrail.