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.
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.
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 oneweb-serviceComponentType pluspersistent-volumeandautoscalerTraits. - Governed à-la-carte capability — a ComponentType's
allowedTraitscontrols 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.
| Part | Field | What it does |
|---|---|---|
| Parameter schema | parameters.openAPIV3Schema | What a developer sets per trait instance (mount path, volume name) |
| Environment schema | environmentConfigs.openAPIV3Schema | What's overridable per environment via the binding (size, storage class) |
| Creates | creates | New Kubernetes resources the Trait adds (PVC, ConfigMap, ServiceMonitor) |
| Patches | patches | Modifications 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:
createsgenerate new resources that don't exist in the base ComponentType. A storage Trait creates aPersistentVolumeClaim; a monitoring Trait creates aServiceMonitor.patchesmodify 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 themaincontainer, 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.instanceNameis what makes the same Trait attachable multiple times. Because the PVC name and the patch both key off it, twopersistent-volumeinstances produce two PVCs and two distinct mounts.- The array-filtered patch path
containers[?(@.name=='main')]/volumeMounts/-finds themaincontainer 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:
- Patching syntax —
the Trait
patchesgrammar. - Authoring ComponentTypes and Traits — the shared schema and templating model.
- CEL reference — context variables and built-ins.
What's next
- Component types — the base templates Traits overlay and the
allowedTraitsthat 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.