Trait Handlers
OAM Built-in Trait Handlers
Package traits implements oam.TraitHandler for the built-in trait types. A trait
decorates or augments a component — adding networking, security, storage, scaling,
or operational behavior. Handlers are registered with the transformer in
pkg/cmd/kurel via RegisterBuiltinTrait(type, handler); each implements
CanHandle + Apply. Some traits are capability-aware (CapabilityRequired)
and draw platform choices (issuer, gateway, secret store) from the ClusterProfile.
Every built-in trait handler also implements oam.PropertySchemaProvider
(PropertySchema()), declaring a constrained schema for its user-facing properties so
the downstream runtime can validate them before invocation. This includes the platform-reserved keys a
handler reads from merged properties (e.g. networkPolicy, allowedHostnameWildcard,
controllerType). Some deeply nested or K8s-adjacent shapes are kept shallow/open
(additionalProperties) rather than modeled field-by-field, but strictness-sensitive traits are
closed: the rbac rule object and the fluxcd-patches patch item and its target selector
enumerate their fields and set additionalProperties: false (unknown keys rejected), matching the
downstream single-owner adoption of these builtins. prune-protection accepts no
properties and so declares an empty schema. Every property (including nested object fields and
array item schemas at every depth) carries a Description, surfaced in the downstream runtime’s generated Handler
API Reference.
Capability-injected fields are not marked Required in a handler’s schema, because
they are supplied by capability rendering (validated in ValidateAndApplyDefaults), not by
the OAM author — e.g. expose.controllerType and the parent certificate.issuerRef are
optional in the user-facing schema (though issuerRef.name stays required when issuerRef
is present). Marking a capability-injected field user-required would make a consumer’s schema
preflight reject every valid use of the trait.
Trait catalog
Networking
type | Produces | Key properties |
|---|---|---|
ingress | Ingress | rules[] (host, paths[]), ingressClassName, tls[], annotations |
httproute | Gateway API HTTPRoute | rules[] (matches/backendRefs/filters/timeouts), hostnames[]; parentRefs[] optional — synthesized from the gatewayName/gatewayNamespace capability when omitted |
expose | Ingress or HTTPRoute | rules[], hostnames[] — controller chosen by ClusterProfile (controllerType) |
networkpolicy | NetworkPolicy | ingress[]/egress[] (from/to, ports) |
cilium-networkpolicy | CiliumNetworkPolicy | name, endpointSelector, ingress/egress (raw Cilium rules) |
Security
type | Produces | Key properties |
|---|---|---|
certificate | cert-manager Certificate | secretName, dnsNames[], duration, renewBefore, privateKey (algorithm/size/encoding/rotationPolicy) (issuer from ClusterProfile) |
rbac | Role/RoleBinding (+ClusterRole/Binding) | rules[] (apiGroups/resources/verbs), clusterWide |
external-secret | ESO ExternalSecret | secretName, data[]/dataFrom[], refreshInterval (store from ClusterProfile or provider) |
security-context | (modifies PodSpec) | psaLevel (restricted|baseline|privileged), optional: runAsNonRoot, allowPrivilegeEscalation, readOnlyRootFilesystem, runAsUser, runAsGroup, fsGroup |
Storage
type | Produces | Key properties |
|---|---|---|
pvc | PersistentVolumeClaim | name, size (optional; policy default storageSize), storageClassName, accessModes[] (policy: maxStorageSize) |
volsync | VolSync ReplicationSource | sourcePVC, schedule, copyMethod, storageClassName, volumeSnapshotClassName, retain.{daily,weekly,monthly} (class fields also supplied via capability rendering; injection is copyMethod-aware) |
Configuration & scaling
type | Produces | Key properties |
|---|---|---|
configmap | ConfigMap (+ optional volume mount) | name, data, mountPath |
scaler | HorizontalPodAutoscaler (+ optional PDB) | minReplicas, maxReplicas (both optional; policy defaults scalerMinReplicas/scalerMaxReplicas, policy cap maxReplicas), cpuUtilization, memoryUtilization, enablePDB |
Operational (FluxCD)
type | Effect | Key properties |
|---|---|---|
fluxcd-patches | Appends Kustomization.spec.patches | patches[] (patch, target) |
fluxcd-postbuild | Sets Kustomization.spec.postBuild | substitute, substituteFrom[] |
prune-protection | Adds kustomize.toolkit.fluxcd.io/prune: disabled | (no properties) |
Capability-aware traits
These require (or optionally use) a ClusterProfile capability, so the platform —
not the app — chooses the implementation:
expose →
controllerType(ingress vs gateway) + gateway/ingress details. On the ingress path, expose is platform-managed for TLS: it derivesspec.tls[]from the rule hosts under a deterministic<component>-tlssecret and emits thecert-manager.io/cluster-issuerannotation from thecertManagerClusterIssuercapability field (empty ⇒ managed TLS disabled). Users do not author the TLS block on the expose trait (use the low-levelingresstrait for full TLS control), but may authorsecretNameto override just the managed secret’s name (still ingress-only, hosts stay rule-derived, and it requires the cluster-issuer capability; asecretNameon the gateway path or without managed TLS is aValidationError). This lets a component carry several expose ingress traits (distinctname/scope) each naming its own cert secret. Both paths validate user hostnames against theallowedHostnameWildcardcapability field (empty ⇒ no validation); a violation is aValidationError. On the ingress path a barehostnames: [...]shorthand is accepted: whenrulesis absent it expands to one rule per host withpath: /+ the component service port (supplyrulesfor finer control; both together keeprulesfor routing while all hosts are still wildcard-validated). Platform-defaultssl-redirect/force-ssl-redirectcome from thesslRedirect/forceSslRedirectcapability fields (author-overridable via the same inline properties; the typed value wins over a raw same-key annotation). External-auth (oauth2-proxy): authoringallowedGroups: [...]on an ingress expose emits the nginxauth-url/auth-signin/auth-response-headersannotations from the capability’sauthURL/authSigninURL/authResponseHeaders(authSigninURLis override-able inline;authURLmust be a bare base URL).allowedGroupsmust be non-empty, and the capability must supplyauthURLor the trait is rejected.certificate →
issuerRef(cert-manager issuer/cluster-issuer).external-secret →
secretStoreRef(or the inlineprovidershorthand).data[]entries derive by absence: a bare- secretKey: FOOdefaultsremoteRef.keyto"<namespace>/<secretName>"andremoteRef.propertytosecretKey; author anyremoteReffield to override. Because absence is meaningful, unknown keys in an entry or itsremoteRefare rejected (naming the supported fields) rather than silently ignored. See External Secret Shorthand.
Auto-synthesized NetworkPolicy
Routing traits (ingress/httproute/expose) can surface platform-reserved
networkPolicy.trafficSources, which the OAM layer collects to synthesize a
matching NetworkPolicy (see pkg/oam/netpol).
When a routing trait’s backendRefs (httproute) or path backend (ingress) names a separate
in-bundle backend Service rather than the exposing component’s own, the synthesized
{comp}-allow-ingress-traffic allow is retargeted onto the backend component’s pods + the
backendRef port — so router→backend traffic is allowed under a namespace default-deny. The backend
Service name is resolved to a sibling OAM component in the same bundle (a component’s Service name
is its BackendServiceName() when it declares one, else its component name); a backendRef that
resolves to no in-bundle component is left authored. Resolution assumes the sibling’s Service
port equals its container port, which holds for all builtin components (e.g. webservice sets
TargetPort == Port).
A backend that names a bare external Service (no owning OAM component in the bundle) cannot be
resolved to a selector by name. To synthesize an allow for it, add an explicit authorable
backendSelector (matchLabels only) beside the backend reference —
rules[].paths[].backendSelector (ingress/expose) or rules[].backendRefs[].backendSelector
(httproute):
This emits a {service}-allow-ingress-traffic policy in the router’s namespace selecting the
backend’s pods on the backend ports. Without a backendSelector, an external backend stays authored
(no selector is ever inferred from the Service name). A backendSelector on a self/implicit backend
is rejected (it could never take effect), and a backendSelector on a ref that resolves to a
sibling component is ignored (component-label retargeting wins). Same-namespace only.
Extending
Custom traits implement oam.TraitHandler (CanHandle + Apply), optionally
CapabilityAware + ValidateAndApplyDefaults for capability validation.
See pkg.go.dev
for the full config-field reference, the OAM model
for the interfaces, and examples/ for runnable applications.
Component attribution
Every trait sub-app config exposes the OAM component it was emitted for via
ComponentName() string (the oam.ComponentNamed interface) — always the component
name, never the sub-app or K8s Service name. Consumers use it to stamp per-resource
provenance (the derived <domain>/component label) without re-deriving the component
from sub-app names, which several handlers author from properties rather than
<component>-<suffix>. The routing traits’ existing TargetComponentName() (used by
auto-NetworkPolicy synthesis) delegates to the same accessor; auto-synthesized
NetworkPolicies target that <domain>/component label by default (domain from
TransformContext.Domain, library default gokure.dev;
TransformContext.ComponentLabelKey-overridable).
Conventions
Handlers use k8s.io/api constants for well-known Kubernetes enum values (access
modes, restart policies, etc.) rather than string literals — never re-define values
that already exist upstream.