Trait Handlers

OAM Built-in Trait Handlers

Go Reference Go Reference

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

typeProducesKey properties
ingressIngressrules[] (host, paths[]), ingressClassName, tls[], annotations
httprouteGateway API HTTPRouterules[] (matches/backendRefs/filters/timeouts), hostnames[]; parentRefs[] optional — synthesized from the gatewayName/gatewayNamespace capability when omitted
exposeIngress or HTTPRouterules[], hostnames[] — controller chosen by ClusterProfile (controllerType)
networkpolicyNetworkPolicyingress[]/egress[] (from/to, ports)
cilium-networkpolicyCiliumNetworkPolicyname, endpointSelector, ingress/egress (raw Cilium rules)

Security

typeProducesKey properties
certificatecert-manager CertificatesecretName, dnsNames[], duration, renewBefore, privateKey (algorithm/size/encoding/rotationPolicy) (issuer from ClusterProfile)
rbacRole/RoleBinding (+ClusterRole/Binding)rules[] (apiGroups/resources/verbs), clusterWide
external-secretESO ExternalSecretsecretName, data[]/dataFrom[], refreshInterval (store from ClusterProfile or provider)
security-context(modifies PodSpec)psaLevel (restricted|baseline|privileged), optional: runAsNonRoot, allowPrivilegeEscalation, readOnlyRootFilesystem, runAsUser, runAsGroup, fsGroup

Storage

typeProducesKey properties
pvcPersistentVolumeClaimname, size (optional; policy default storageSize), storageClassName, accessModes[] (policy: maxStorageSize)
volsyncVolSync ReplicationSourcesourcePVC, schedule, copyMethod, storageClassName, volumeSnapshotClassName, retain.{daily,weekly,monthly} (class fields also supplied via capability rendering; injection is copyMethod-aware)

Configuration & scaling

typeProducesKey properties
configmapConfigMap (+ optional volume mount)name, data, mountPath
scalerHorizontalPodAutoscaler (+ optional PDB)minReplicas, maxReplicas (both optional; policy defaults scalerMinReplicas/scalerMaxReplicas, policy cap maxReplicas), cpuUtilization, memoryUtilization, enablePDB

Operational (FluxCD)

typeEffectKey properties
fluxcd-patchesAppends Kustomization.spec.patchespatches[] (patch, target)
fluxcd-postbuildSets Kustomization.spec.postBuildsubstitute, substituteFrom[]
prune-protectionAdds 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:

  • exposecontrollerType (ingress vs gateway) + gateway/ingress details. On the ingress path, expose is platform-managed for TLS: it derives spec.tls[] from the rule hosts under a deterministic <component>-tls secret and emits the cert-manager.io/cluster-issuer annotation from the certManagerClusterIssuer capability field (empty ⇒ managed TLS disabled). Users do not author the TLS block on the expose trait (use the low-level ingress trait for full TLS control), but may author secretName to override just the managed secret’s name (still ingress-only, hosts stay rule-derived, and it requires the cluster-issuer capability; a secretName on the gateway path or without managed TLS is a ValidationError). This lets a component carry several expose ingress traits (distinct name/scope) each naming its own cert secret. Both paths validate user hostnames against the allowedHostnameWildcard capability field (empty ⇒ no validation); a violation is a ValidationError. On the ingress path a bare hostnames: [...] shorthand is accepted: when rules is absent it expands to one rule per host with path: / + the component service port (supply rules for finer control; both together keep rules for routing while all hosts are still wildcard-validated). Platform-default ssl-redirect / force-ssl-redirect come from the sslRedirect / forceSslRedirect capability fields (author-overridable via the same inline properties; the typed value wins over a raw same-key annotation). External-auth (oauth2-proxy): authoring allowedGroups: [...] on an ingress expose emits the nginx auth-url / auth-signin / auth-response-headers annotations from the capability’s authURL / authSigninURL / authResponseHeaders (authSigninURL is override-able inline; authURL must be a bare base URL). allowedGroups must be non-empty, and the capability must supply authURL or the trait is rejected.

  • certificateissuerRef (cert-manager issuer/cluster-issuer).

  • external-secretsecretStoreRef (or the inline provider shorthand).

    data[] entries derive by absence: a bare - secretKey: FOO defaults remoteRef.key to "<namespace>/<secretName>" and remoteRef.property to secretKey; author any remoteRef field to override. Because absence is meaningful, unknown keys in an entry or its remoteRef are 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):

paths:
  - path: /
    backend: external-svc      # a Service with no OAM component
    port: 8081
    backendSelector:
      matchLabels:
        app.kubernetes.io/name: external

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.