This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Mutate

Set resource properties during admission with typed mutations

Use spec.rules[].mutate to set resource properties during admission. It is an ordered list of entries, each with an optional action and a typed resource block. ingress.class defaults or replaces class references on Ingress and Gateway creation and updates. ingress.listenerSets.parentRef supplies Gateway references on ListenerSet creation and updates. workloads applies to Pods on creation; security.readOnlyRootFilesystem and registries.imagePullPolicy also apply to newly added ephemeral containers. Mutation targets select compatible Pod locations, with pod including all container groups.

mutate and enforce are sibling keys. Mutation changes the resource; enforcement validates the result. Either can be used alone, and mutated values remain subject to applicable enforcement policies.

DeviceClass defaults fill missing class names on ResourceClaim and ResourceClaimTemplate creation in selected namespaces.

See Order and scope for the admission sequence and Conditions for conditional mutations.

Action

Each mutate entry supports an action field:

ActionBehavior
merge (default)Apply supplied values while retaining unrelated settings. Map keys are set, list entries are added or updated, and affinity restrictions are combined according to the property’s rules.
replaceReplace each supplied property in full. Other properties remain unchanged.

The action applies to every property in that entry. Use separate entries when properties need different actions:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: merge
          workloads:
            placement:
              nodeSelector:
                kubernetes.io/os: linux
        - action: replace
          workloads:
            placement:
              tolerations:
                - key: infrastructure.example.com/dedicated
                  operator: Equal
                  value: shared
                  effect: NoSchedule

This retains other node-selector keys, but replaces the entire tolerations list. Other admission controllers can subsequently add tolerations.

Replacement happens at the supplied property’s boundary. For example, placement.nodeSelector is replaced as one map and placement.affinity as one object, including all its branches. The placement reference explains which list entries match during a merge and how required affinity is combined. Scalar settings such as security.hostUsers, security.readOnlyRootFilesystem, and registries.imagePullPolicy overwrite selected values with either action. classes.priority is assigned by merge only when the Pod has no priority class of its own, and by replace always. Ingress and Gateway class names are scalar defaults: merge preserves a non-empty class, while replace sets the configured name on every matching write. ListenerSet parent references are applied as complete references: merge fills only an absent or null reference, while replace replaces it in full. DeviceClass defaults support only merge: they fill missing class names and preserve every explicit name.

Omitted and empty values

Value in the mutationEffect
Property omitted or nullLeave the existing property unchanged with either action.
Empty placement.nodeSelector, placement.tolerations, placement.topologySpreadConstraints, placement.affinity, or registries.imagePullSecrets with replaceClear that property.
Empty placement: {}, security: {}, registries: {}, classes: {}, ingress: {}, ingress.listenerSets: {}, or devices: {}Supply no changes; another mutation property must be supplied.
Empty ingress.listenerSets.parentRef: {}Invalid: name is required.
Empty ingress.class: "", or ingress.types without a non-empty ingress.classInvalid: configure the class name and its target kinds together.
security.hostUsers: false or security.readOnlyRootFilesystem: falseSet an explicit Boolean value; false is not an omission.

For example, this clears node selectors and affinity while retaining tolerations and topology-spread constraints:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: replace
          workloads:
            placement:
              nodeSelector: {}
              affinity: {}

1 - Workloads

Configure Pod placement, security, image pull policies, and priority classes with ordered mutations

Workload mutations set Pod properties under spec.rules[].mutate[].workloads using native Kubernetes Pod syntax. Use placement settings to select schedulers, assign node pools, add tolerations, spread workloads across topology domains, and configure affinity. Use security settings to configure Pod user namespaces, seccomp/AppArmor profiles, and read-only container root filesystems. Use registry settings to configure container image pull policies and Pod image pull secret references. Use class settings to assign a PriorityClass. Placement enforcement validates the resulting placement settings using matchers.

This page covers configuration and mutation behavior. The Reference provides a complete Tenant combining all workload mutation properties with placement, security-profile, image pull policy, and priority class enforcement.

PropertyField under workloadsPod fieldEnforcement
Schedulerplacement.schedulerspec.schedulerNameScheduler matchers
Node selectorsplacement.nodeSelectorspec.nodeSelectorNode-selector matchers
Tolerationsplacement.tolerationsspec.tolerationsToleration matchers
Topology spread constraintsplacement.topologySpreadConstraintsspec.topologySpreadConstraintsSpread matchers
Affinityplacement.affinityspec.affinityAffinity matchers
Seccompsecurity.seccompProfilespec.securityContext.seccompProfileSeccomp matchers
AppArmorsecurity.appArmorProfilespec.securityContext.appArmorProfileAppArmor matchers
Read-only root filesystemsecurity.readOnlyRootFilesystemSelected containers: securityContext.readOnlyRootFilesystemMutation only.
Image pull secretsregistries.imagePullSecretsspec.imagePullSecretsMutation only.
Image pull policyregistries.imagePullPolicySelected containers: imagePullPolicyRegistry pull policies
Host user namespacesecurity.hostUsersspec.hostUsersMutation only.
Priority classclasses.priorityspec.priorityClassName, with its spec.priority and spec.preemptionPolicyPriority class matchers

Configure workload mutations

Each mutation entry has an optional action and a workloads block containing at least one property to set. Scheduling fields live under workloads.placement; security fields live under workloads.security; class assignments live under workloads.classes. targets stays directly under workloads and applies to all configured groups. The action applies to each supplied field inside a group: replacing placement.scheduler preserves an omitted placement.nodeSelector and all omitted security fields. Empty placement: {} or security: {} groups supply no mutations; use an explicit empty map or list on a supported field to clear it. For conditional mutations, see Conditions.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          environment: production
      mutate:
        - action: merge
          workloads:
            placement:
              nodeSelector:
                kubernetes.io/os: linux
      enforce:
        action: allow
        workloads:
          placement:
            nodeSelector:
              - key:
                  exact: [kubernetes.io/os]
                values:
                  exact: [linux]

In selected namespaces, this rule sets the Linux node selector. The allow rule then rejects any additional node-selector entries. mutate and enforce are sibling keys and can be used independently. enforce.action does not select or disable mutations.

Workload mutations apply on Pod creation. security.readOnlyRootFilesystem and registries.imagePullPolicy also apply to newly added ephemeral containers. See Order and scope for the mutate-then-enforce sequence, rule selection, and supported operations.

Targets

Set mutate[].workloads.targets to select where the entry applies. Omitting targets, using targets: [], or selecting pod includes all compatible locations. For mutation, pod includes every regular, init, and ephemeral container. Native sidecars belong to the init-container group.

The table lists supported mutation targets. ✅ indicates mutation support; ❌ means that property cannot be mutated through that target. Controller and volume targets are supported by enforcement but are rejected by the mutation API.

TargetPlacement / Image pull secrets / Priority classsecurity.hostUsersSeccomp / AppArmorsecurity.readOnlyRootFilesystem / registries.imagePullPolicy
Omitted, [], or pod✅ Pod-level fields✅ Pod user namespace✅ Pod-level profiles only✅ All container groups
pod/containers❌❌❌✅ Regular containers
pod/initcontainers❌❌❌✅ Init containers and native sidecars
pod/ephemeralcontainers❌❌❌✅ Newly added ephemeral containers

Placement includes placement.scheduler, placement.nodeSelector, placement.tolerations, placement.topologySpreadConstraints, and placement.affinity. These properties, security.hostUsers, security.seccompProfile, security.appArmorProfile, registries.imagePullSecrets, and classes.priority require pod or omitted/empty targets. Seccomp and AppArmor mutation preserves explicit container profiles with both merge and replace; container-specific profile mutation is not supported.

security.readOnlyRootFilesystem and registries.imagePullPolicy support container-specific mutation targets. Use separate mutation entries to combine Pod-level settings with a narrower container selection. Controller and volume targets are not supported for typed workload mutations.

Targets combine by inclusion: listing pod alongside a narrower target still selects all groups. A targets-only mutation is invalid because it has no property to set. Enforcement targets are configured independently and have property-specific matching semantics.

Regular and init containers are mutated on Pod creation. Ephemeral containers are mutated only when newly added through UPDATE pods/ephemeralcontainers. An existing ephemeral container is left unchanged, even if the rule changed since it was added. Other Pod updates, deletes, and subresources do not apply these mutations. On an ephemeral update, the entry’s conditions see the full current Pod, including earlier mutations, but only the new ephemeral containers' securityContext.readOnlyRootFilesystem and imagePullPolicy values can change.

Placement

Scheduler

placement.scheduler sets the Pod’s spec.schedulerName. Use the name of a scheduler configured in your cluster; Capsule selects the scheduler but does not deploy it. See Kubernetes multiple schedulers.

To select a scheduler for every new Pod covered by the rule, use replace:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: replace
          workloads:
            placement:
              scheduler: tenant-scheduler
ActionBehavior
mergeFill an empty spec.schedulerName. Preserve every non-empty value, including default-scheduler.
replaceSet spec.schedulerName to the configured value when the entry’s conditions match.

Omitting placement.scheduler or setting it to null leaves the current value unchanged. An empty or blank configured name is invalid. To select the built-in scheduler, set scheduler: default-scheduler explicitly.

Default a scheduler with a condition

Kubernetes fills an omitted or empty spec.schedulerName with default-scheduler before Capsule receives the admission request. To use a tenant scheduler as the default while preserving custom scheduler names, combine replace with a mutation condition:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: replace
          conditions:
            - name: default-scheduler
              expression: >-
                !has(object.spec.schedulerName) || object.spec.schedulerName in ['', 'default-scheduler']                
          workloads:
            placement:
              scheduler: tenant-scheduler
Scheduler in the submitted PodResult with this condition
Omitted or emptytenant-scheduler
default-schedulertenant-scheduler
batch-schedulerbatch-scheduler

The condition cannot distinguish an omitted value from an explicitly selected default-scheduler: both reach admission with the built-in name. This rule deliberately replaces both. A custom scheduler supplied by the user or an earlier mutation makes the condition false and is retained.

Keep this scheduler default in its own mutation entry. Conditions gate every property in an entry, so adding node selectors or tolerations to it would make those changes depend on the scheduler condition too. Namespace selection and audience still determine which rules apply. Only new Pods are changed; existing Pods and controller templates retain their settings.

The final scheduler name remains subject to scheduler enforcement. Allow the configured default in any applicable scheduler allow-list, or admission will reject the mutated Pod.

Node selectors

placement.nodeSelector is a map of node-label keys and values. Every entry must match a node for the Pod to be scheduled there.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: merge
          workloads:
            placement:
              nodeSelector:
                infrastructure.example.com/pool: shared
ActionBehavior
mergeSet the supplied keys, overwriting their existing values. Retain other keys.
replaceReplace the entire map. Use nodeSelector: {} to clear it.

For a Pod whose selector is {infrastructure.example.com/pool: private, disk: ssd}, the example produces:

ActionResulting node selector
merge{infrastructure.example.com/pool: shared, disk: ssd}
replace{infrastructure.example.com/pool: shared}

Later mutations can overwrite the same key. For defaults, see set a value only when it is missing. Use node-selector enforcement to restrict which keys and values remain on the admitted Pod.

Tolerations

placement.tolerations is a list of native Pod tolerations. A toleration permits scheduling onto a node with a matching taint; it does not require that node. Use a node selector or required node affinity to restrict placement to a node pool.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: merge
          workloads:
            placement:
              tolerations:
                - key: infrastructure.example.com/dedicated
                  operator: Equal
                  value: shared
                  effect: NoExecute
                  tolerationSeconds: 60
ActionBehavior
mergeAdd or update the toleration identified by key + operator + value + effect. An omitted operator means Equal.
replaceReplace the entire list, including tolerations with other identities. Use tolerations: [] to clear it.

The duration is not part of the merge identity. A matching toleration receives the supplied tolerationSeconds. Omitting the duration removes an existing limit and makes the toleration unlimited.

Existing tolerationSupplied tolerationResult with merge
pool=private, Equal, NoSchedulepool=shared, Equal, NoScheduleBoth remain because their values differ.
pool=shared, Equal, NoExecute, 30 secondsSame identity, 60 secondsOne toleration with a 60-second duration.
pool=shared, Equal, NoExecute, 30 secondsSame identity, no durationOne toleration with unlimited duration.

Changing a key, operator, value, or effect adds another toleration under merge. Use replace when only the supplied list should remain. Other admission controllers may subsequently inject additional tolerations. All final entries are subject to toleration enforcement, including Kubernetes-injected entries.

Topology spread constraints

placement.topologySpreadConstraints is a list of native Pod spread constraints. Each constraint specifies a topology domain and the Pods to count when spreading.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: merge
          workloads:
            placement:
              topologySpreadConstraints:
                - topologyKey: kubernetes.io/hostname
                  whenUnsatisfiable: ScheduleAnyway
                  maxSkew: 1
                  labelSelector:
                    matchLabels:
                      app: checkout

This expresses a preference to spread Pods labeled app: checkout across hosts. Use DoNotSchedule when the spread constraint must prevent scheduling that would violate it.

ActionBehavior
mergeAdd or replace the entire constraint identified by topologyKey + whenUnsatisfiable. Retain constraints with other identities.
replaceReplace the entire list. Use topologySpreadConstraints: [] to clear it.

If the Pod already has a kubernetes.io/hostname / ScheduleAnyway constraint with maxSkew: 3, the example replaces it with maxSkew: 1 and the supplied selector. Old selector fields and optional fields are not carried forward. An existing topology.kubernetes.io/zone / DoNotSchedule constraint remains with merge and is removed with replace unless it is also supplied.

Changing whenUnsatisfiable creates a different merge identity, even if the topology key is unchanged. For allowed topology keys, skew ranges, and selector operators, see spread enforcement.

Affinity

placement.affinity uses the native Pod structure with three branches: nodeAffinity, podAffinity, and podAntiAffinity. Each can contain required and preferred terms.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: merge
          workloads:
            placement:
              affinity:
                nodeAffinity:
                  requiredDuringSchedulingIgnoredDuringExecution:
                    nodeSelectorTerms:
                      - matchExpressions:
                          - key: topology.kubernetes.io/zone
                            operator: In
                            values: [zone-a, zone-b]
                podAffinity:
                  preferredDuringSchedulingIgnoredDuringExecution:
                    - weight: 50
                      podAffinityTerm:
                        topologyKey: kubernetes.io/hostname
                        labelSelector:
                          matchLabels:
                            app: cache
                podAntiAffinity:
                  preferredDuringSchedulingIgnoredDuringExecution:
                    - weight: 100
                      podAffinityTerm:
                        topologyKey: kubernetes.io/hostname
                        labelSelector:
                          matchLabels:
                            app: checkout

This requires a node in zone-a or zone-b, prefers a host with cache Pods, and prefers to separate checkout Pods across hosts. The referenced node labels must exist on the intended nodes. Omitting namespace selection in these Pod-affinity terms limits their Pod matching to the incoming Pod’s namespace.

Merge

Affinity termBehavior with merge
Required node affinityRequire both the existing restrictions and the supplied restrictions. Combine existing and supplied alternatives to preserve that AND relationship.
Required Pod affinity or anti-affinityAdd distinct complete terms. All required terms remain required; identical terms are retained once.
Preferred affinity of any typeAdd distinct complete terms or replace the weight of a matching term.

For example, existing node alternatives (disk=ssd OR arch=arm64) combined with supplied zone=a become (disk=ssd AND zone=a) OR (arch=arm64 AND zone=a). Required restrictions accumulate across applicable mutations. Capsule rejects combinations that would produce more than 256 node-selector alternatives. An empty node-selector term matches no nodes.

Preferred-term identity includes the complete node preference or Pod-affinity term, excluding its weight. Requirement and selector-value order are normalized for matching. Supplying the same term with weight 80 updates an existing weight 50; changing the selector adds a distinct preferred term.

Replace

replace replaces all of placement.affinity, including branches omitted from the mutation. Supplying only nodeAffinity also removes existing podAffinity and podAntiAffinity. Use affinity: {} to clear all three branches.

For allowed types, required/preferred modes, selector requirements, and namespace scope, see affinity enforcement. Its matcher syntax uses one flat list across all three affinity types.

Security

Seccomp and AppArmor mutations set only the Pod-level profile on creation. Use targets: [pod] or omit mutation targets for these Pod-level properties. enforce.workloads.targets selects enforcement locations independently. Both merge and replace preserve explicit regular, init, and ephemeral container profiles. Containers without an override inherit the Pod default. See the Pod targets table and profile target example for how enforcement checks those defaults and overrides.

Read-only root filesystem

security.readOnlyRootFilesystem sets the Boolean on every selected container’s securityContext. Like security.hostUsers, both merge and replace set the supplied value, including an explicit false. Omitted or null leaves the value unchanged. Later matching entries can overwrite earlier values; all other container security-context fields are preserved.

This full Tenant makes all container root filesystems read-only in namespaces labeled filesystem-profile: readonly. Namespaces labeled filesystem-profile: init-only apply the setting only to init containers, including native sidecars. Other namespaces keep their submitted values.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          filesystem-profile: readonly
      mutate:
        - action: merge
          workloads:
            targets: [pod]
            security:
              readOnlyRootFilesystem: true
    - namespaceSelector:
        matchLabels:
          filesystem-profile: init-only
      mutate:
        - action: replace
          workloads:
            targets: [pod/initcontainers]
            security:
              readOnlyRootFilesystem: true

In the first profile, pod also selects new ephemeral containers. Choose pod/containers for regular containers only or pod/ephemeralcontainers for new debug containers only. Neither the rule nor a namespace-label change rewrites existing containers. Pods declaring spec.os.name: windows are skipped.

A read-only root filesystem does not make mounted volumes read-only. Applications that write to paths such as /tmp need suitable writable volume mounts. See the Kubernetes security-context documentation. This field is a mutation setting; there is no corresponding enforce.workloads.security.readOnlyRootFilesystem matcher.

Host user namespace

security.hostUsers selects whether the Pod uses the host user namespace.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: merge
          workloads:
            security:
              hostUsers: false
Supplied valueBehavior with merge or replace
falseRequest a separate user namespace for the Pod.
trueUse the host user namespace.

Both actions overwrite an existing Boolean. A later entry can change false to true or the reverse. This property is independent of a container’s runAsUser and has no corresponding enforce.workloads.security.hostUsers matcher.

The Kubernetes version, operating system, and container runtime must support the requested setting. Kubernetes validates incompatible Pod settings; Capsule does not adjust other security or host-namespace fields. See Kubernetes user namespaces.

Seccomp

security.seccompProfile sets spec.securityContext.seccompProfile on new Linux Pods. Containers inherit this Pod default unless their own security context supplies a seccomp profile. This includes regular, init, and ephemeral containers.

PropertyMeaning
type: RuntimeDefaultUse the container runtime’s default seccomp profile. Omit localhostProfile.
type: LocalhostUse a seccomp profile file on the node. Set localhostProfile to its relative path.
type: UnconfinedDisable seccomp filtering. Omit localhostProfile.

Seccomp mutation behavior

ActionBehavior
mergeSet the configured profile only when the Pod has no spec.securityContext.seccompProfile. Preserve any supplied profile, including its localhost path.
replaceReplace the complete Pod profile with the configured type and localhost path.
Property omittedPreserve the Pod’s seccomp profile.

Merge treats the profile as one property: it does not fill a missing localhostProfile inside an already supplied profile. A Localhost profile must include its path. Replacing Localhost with RuntimeDefault removes the old path.

Other security-context fields and explicit container profiles are preserved. Use seccomp enforcement to prevent container overrides from bypassing the intended profile. Mutations use the existing conditions and order, run only on Pod creation, and skip Pods declaring spec.os.name: windows. They do not change running Pods or controller templates.

Default to RuntimeDefault

This complete Tenant defaults seccomp in namespaces labeled security-profile: confined. The allow rule requires every container’s effective profile to be RuntimeDefault, including containers added later through pods/ephemeralcontainers.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          security-profile: confined
      mutate:
        - action: merge
          workloads:
            security:
              seccompProfile:
                type: RuntimeDefault
      enforce:
        action: allow
        workloads:
          security:
            seccompProfiles:
              - types: [RuntimeDefault]

An explicit container-level Unconfined profile remains unchanged by mutation and is rejected by enforcement. Namespaces without the selector label do not receive either rule effect.

Set a local seccomp path

Set type: Localhost together with localhostProfile. The path is relative to the kubelet’s seccomp directory and must not be absolute or contain .. path segments. For example, profiles/solar.json refers to /var/lib/kubelet/seccomp/profiles/solar.json when the kubelet uses its default root directory. It is a file on the node, independent of the container’s filesystem. See the Kubernetes seccomp tutorial.

This Tenant replaces the Pod default with that local profile and allows only that profile for the effective container checks:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          security-profile: local-seccomp
      mutate:
        - action: replace
          workloads:
            security:
              seccompProfile:
                type: Localhost
                localhostProfile: profiles/solar.json
      enforce:
        action: allow
        workloads:
          security:
            seccompProfiles:
              - types: [Localhost]
                localhostProfiles:
                  - exact: [profiles/solar.json]

Use action: merge instead to supply this profile only when the Pod profile is absent. With the allow-list above, an existing different profile is then preserved by merge and rejected by enforcement.

Install the file on every eligible node before using the rule, or combine the rule with placement that selects prepared nodes. Capsule writes the profile reference; it does not distribute or inspect the file. A Pod can pass admission and still fail to start if the profile is unavailable on its node.

AppArmor

security.appArmorProfile sets spec.securityContext.appArmorProfile on new Linux Pods. The Pod value supplies the default for containers that have no AppArmor override. Use this mutation only for workloads placed on nodes with AppArmor support. Explicit RuntimeDefault can prevent a Pod from starting on a node without that support. See the AppArmor prerequisites.

PropertyMeaning
type: RuntimeDefaultUse the container runtime’s default AppArmor profile. Omit localhostProfile.
type: LocalhostUse an AppArmor profile already loaded on the node. Set localhostProfile to its loaded name.
type: UnconfinedDisable AppArmor confinement. Omit localhostProfile.

AppArmor mutation behavior

ActionBehavior
mergeSet the configured profile only when the Pod has no spec.securityContext.appArmorProfile. Preserve any supplied profile, including its localhost name.
replaceReplace the complete Pod profile with the configured type and localhost name.
Property omittedPreserve the Pod’s AppArmor profile.

Merge does not complete a partially supplied Localhost profile. Replacing a Localhost profile with RuntimeDefault removes the old localhostProfile. Other security-context fields and container overrides, including legacy AppArmor annotations, remain unchanged. Pair mutation with AppArmor enforcement to validate these effective container choices.

The existing conditions and order apply. Mutations run only on Pod creation and skip Pods declaring spec.os.name: windows; they do not change running Pods or controller templates.

Default the AppArmor profile

This Tenant supplies RuntimeDefault and requires it for every effective container profile in the selected namespaces:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          security-profile: apparmor
      mutate:
        - action: merge
          workloads:
            security:
              appArmorProfile:
                type: RuntimeDefault
      enforce:
        action: allow
        workloads:
          security:
            appArmorProfiles:
              - types: [RuntimeDefault]

Set a local AppArmor profile

For AppArmor, localhostProfile is the loaded profile name, such as solar-confined. It is not a filesystem path to the profile definition. The name must match the profile loaded on the node. See Specifying AppArmor confinement.

This Tenant replaces the Pod default with that name and restricts effective container profiles to the same choice:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          security-profile: local-apparmor
      mutate:
        - action: replace
          workloads:
            security:
              appArmorProfile:
                type: Localhost
                localhostProfile: solar-confined
      enforce:
        action: allow
        workloads:
          security:
            appArmorProfiles:
              - types: [Localhost]
                localhostProfiles:
                  - exact: [solar-confined]

Use action: merge to preserve an existing Pod profile instead of replacing it; the enforcement rule still rejects any different effective profile. Load solar-confined on every eligible node before applying the rule, or restrict placement to prepared nodes. Capsule does not load AppArmor profiles or verify that their definitions are consistent across nodes.

Registries

Configure registry-related mutations under mutate[].workloads.registries. Set imagePullPolicy for selected containers and imagePullSecrets for the Pod. The targets table shows which locations support each property. Both settings apply to Linux and Windows Pods.

Image pull policy

Set registries.imagePullPolicy to one of the native Kubernetes values:

ValueContainer image behavior
AlwaysResolve the image through the registry at each container start; cached image layers can still be reused.
IfNotPresentPull the image when it is not already available on the node.
NeverUse an image already available on the node; the container cannot start if it is missing.

See Kubernetes image pull policies for runtime behavior. Capsule changes the container’s policy; it does not pull images or create registry credentials.

Mutation configurationBehavior
action: merge or omitted actionOverwrite the selected containers’ pull policies.
action: replaceOverwrite the selected containers’ pull policies.
registries or imagePullPolicy omitted or nullPreserve existing pull policies.
registries: {}Leave pull policies unchanged. The mutation entry still needs a property to set.
Empty string or a value other than Always, IfNotPresent, or NeverReject the rule configuration.

Kubernetes fills an omitted pull policy before mutation. Capsule overwrites both that default and an explicit user value with either action: merge does not mean “only when unset” for this property. Later applicable mutation entries can overwrite it again. Conditions gate the whole entry and see changes made by preceding entries.

registries is a settings object. The mutation applies the same policy to every selected container, regardless of its image registry. To restrict image sources or validate the resulting pull policy, use registry enforcement, where registries is a list of matchers with an optional policy allowlist.

Apply different namespace profiles

This Tenant uses Always for all container groups in namespaces labeled image-profile: always-pull and allows images only from registry.k8s.io. Namespaces labeled image-profile: init-only receive the mutation only for init containers and native sidecars, preserving regular-container policies.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          image-profile: always-pull
      mutate:
        - action: replace
          workloads:
            targets: [pod]
            registries:
              imagePullPolicy: Always
      enforce:
        action: allow
        workloads:
          targets: [pod/containers, pod/initcontainers, pod/ephemeralcontainers]
          registries:
            - exact: [registry.k8s.io]
              policy: [Always]
    - namespaceSelector:
        matchLabels:
          image-profile: init-only
      mutate:
        - action: merge
          workloads:
            targets: [pod/initcontainers]
            registries:
              imagePullPolicy: Always
RequestResult from these rules
New Pod in always-pull, with omitted, Never, or IfNotPresent policiesRegular and init containers receive Always; images must pass the registry allowlist.
New ephemeral container in always-pullThe new container receives Always; registry enforcement still applies.
New Pod in init-onlyInit containers and native sidecars receive Always; regular containers keep their policies.
Namespace with neither profile labelNeither rule applies.

Existing containers and stored controller templates are unchanged. Pods created by controllers receive the mutation when admitted. Ordinary Pod updates do not rewrite pull policies, and ephemeral-container updates change only newly added containers. Image-volume pull policies are outside the mutation targets.

Image pull secrets

registries.imagePullSecrets sets the Pod’s spec.imagePullSecrets using a list of references with a name field. This is a Pod-level setting: use targets: [pod], targets: [], or omit targets. Container-only targets are rejected, including when clearing the list.

ConfigurationBehavior
merge or omitted actionKeep each existing name once, in first-occurrence order, then append missing configured names.
replaceSet the complete list to the configured references.
imagePullSecrets: [] with replaceClear the Pod’s image pull secret references.
imagePullSecrets: [] with mergeRemove duplicate references; otherwise preserve the existing list.
Property omitted or nullPreserve the list with either action.

Merge retains each name already supplied on the Pod once, including ServiceAccount defaults. Replace can remove those references. Later mutation entries see and can change the resulting list. Repeated names on the incoming Pod are removed, and overlapping rules or repeated admission do not introduce duplicates. Omitting the property leaves the list untouched. Each entry accepts up to 64 references with unique, valid Secret names; duplicate configured names are rejected for both actions.

Referenced Secrets must exist in the Pod’s namespace and contain suitable registry credentials. Kubernetes accepts kubernetes.io/dockerconfigjson or kubernetes.io/dockercfg Secrets for this purpose; see image pull secrets. Capsule writes only the references: it does not create, copy, read, or verify the Secrets. Provision the credentials in each selected namespace separately, for example with TenantResource or GlobalTenantResource.

This Tenant adds a shared reference in one namespace profile, replaces the list in another, and clears it in a third. Create platform-registry or team-registry in the corresponding namespaces before workloads need to pull private images.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          pull-credentials: shared
      mutate:
        - action: merge
          workloads:
            targets: [pod]
            registries:
              imagePullSecrets:
                - name: platform-registry
    - namespaceSelector:
        matchLabels:
          pull-credentials: dedicated
      mutate:
        - action: replace
          workloads:
            registries:
              imagePullSecrets:
                - name: team-registry
    - namespaceSelector:
        matchLabels:
          pull-credentials: none
      mutate:
        - action: replace
          workloads:
            registries:
              imagePullSecrets: []

These rules apply only when a Pod is created, including Pods created by controllers. They do not rewrite controller templates, ServiceAccounts, existing Pods, or the secret list when adding ephemeral containers. Configure both imagePullPolicy and imagePullSecrets in one registries block when using the Pod target. For a container-specific pull policy, use a separate mutation entry.

Classes

Configure the classes a Pod refers to under mutate[].workloads.classes.

Priority class

classes.priority sets the Pod’s spec.priorityClassName. Capsule also writes the priority and preemptionPolicy of that PriorityClass, because Kubernetes rejects a Pod whose priority does not belong to its class. Use the name of a PriorityClass that exists in the cluster; Capsule assigns the class but does not create it.

To give Pods a namespace default when they name no class, use merge:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - mutate:
        - action: merge
          workloads:
            classes:
              priority: solar-standard
ActionBehavior
merge or omitted actionAssign the class when the Pod has none of its own. Preserve a class the Pod names.
replaceAssign the class to every Pod covered by the entry.

Omitting classes.priority or setting it to null leaves the Pod unchanged. An empty name is rejected, and the name must be a valid PriorityClass name. This is a Pod-level setting: use targets: [pod], targets: [], or omit targets.

Classes assigned by Kubernetes

Kubernetes assigns the PriorityClass marked globalDefault: true to every Pod that names no class, before Capsule receives the admission request. merge counts that class as omitted, so a namespace default takes precedence over the cluster default:

Class on the Pod at admissionmergereplace
NoneConfigured classConfigured class
The cluster’s global defaultConfigured classConfigured class
Another classPreservedConfigured class
The configured classUnchangedUnchanged

Capsule cannot distinguish an omitted class from the global default named explicitly: both reach admission with the same value, and merge replaces both. A class is judged by its current globalDefault flag on the API server, so promoting or demoting a PriorityClass takes effect on the next Pod.

When several entries set a class, the first applicable merge decides and later merge entries keep its result. A later replace still overrides it. Conditions gate the whole entry and see the class set by preceding entries.

Missing classes

If the configured PriorityClass does not exist, Capsule rejects the Pod with an error naming the rule, such as rules[0].mutate[0].workloads.classes.priority: PriorityClass "solar-standard" does not exist. Create the class before a rule that refers to it selects namespaces. Pods that keep their own class under merge are unaffected.

The name supports Tenant/Namespace templates, for example priority: '{{ .tenant.metadata.name }}-standard'. The rendered value must be a valid PriorityClass name before the rule becomes ready.

Default and restrict classes per namespace

This Tenant gives production namespaces a default they may override with one other class, and forces a low-priority class in development namespaces:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          environment: production
      mutate:
        - action: merge
          workloads:
            classes:
              priority: solar-standard
      enforce:
        action: allow
        workloads:
          classes:
            priority:
              - exact: [solar-standard, solar-critical]
    - namespaceSelector:
        matchLabels:
          environment: development
      mutate:
        - action: replace
          workloads:
            classes:
              priority: solar-low
RequestResult from these rules
New Pod in production without a class, or with the cluster’s global defaultReceives solar-standard.
New Pod in production naming solar-criticalPreserved and allowed.
New Pod in production naming another classPreserved by merge, then denied by the allow-list.
New Pod in development, with any class or noneReceives solar-low.
Namespace with neither labelNeither rule applies.

These rules apply only when a Pod is created, including Pods created by controllers. Stored controller templates and existing Pods keep their class. The final class remains subject to priority class enforcement: allow the configured class in any applicable allow-list, or admission rejects the mutated Pod. An enforcement rule that targets a controller requires its template to name a class, because this mutation does not reach templates.

Reference

This complete Tenant combines every supported workload mutation property with placement, security-profile, image pull policy, and priority class enforcement. It selects namespaces labeled example.com/application: checkout and configures workloads for Linux nodes.

The tenant-scheduler scheduler, the checkout-standard PriorityClass, node pool, and zone labels must exist in the cluster. Eligible nodes must support user namespaces and AppArmor. Provision the checkout-registry image pull Secret in each selected namespace. Application Pods should carry app.kubernetes.io/part-of: checkout so the spread and Pod-affinity selectors describe the intended workload.

The first mutation entry replaces the built-in scheduler default while preserving custom scheduler names. The second entry independently applies the remaining placement settings, supplies missing Pod-level security profiles, and sets all selected container root filesystems read-only and image pull policies to Always. It also adds checkout-registry to the Pod’s image pull secret references and assigns checkout-standard to Pods without a priority class. Provide writable volume mounts for application paths that need them.

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: checkout
spec:
  owners:
    - kind: User
      name: checkout-owner
  rules:
    - namespaceSelector:
        matchLabels:
          example.com/application: checkout
      mutate:
        - action: replace
          conditions:
            - name: default-scheduler
              expression: >-
                !has(object.spec.schedulerName) || object.spec.schedulerName in ['', 'default-scheduler']                
          workloads:
            placement:
              scheduler: tenant-scheduler
        - action: merge
          workloads:
            targets: [pod]
            security:
              readOnlyRootFilesystem: true
              hostUsers: false
              seccompProfile:
                type: RuntimeDefault
              appArmorProfile:
                type: RuntimeDefault
            registries:
              imagePullPolicy: Always
              imagePullSecrets:
                - name: checkout-registry
            classes:
              priority: checkout-standard
            placement:
              nodeSelector:
                kubernetes.io/os: linux
                infrastructure.example.com/pool: shared
              tolerations:
                - key: infrastructure.example.com/dedicated
                  operator: Equal
                  value: shared
                  effect: NoSchedule
              # Application Pods must carry app.kubernetes.io/part-of: checkout.
              topologySpreadConstraints:
                - topologyKey: topology.kubernetes.io/zone
                  whenUnsatisfiable: DoNotSchedule
                  maxSkew: 1
                  labelSelector:
                    matchLabels:
                      app.kubernetes.io/part-of: checkout
              affinity:
                nodeAffinity:
                  requiredDuringSchedulingIgnoredDuringExecution:
                    nodeSelectorTerms:
                      - matchExpressions:
                          - key: topology.kubernetes.io/zone
                            operator: In
                            values: [zone-a, zone-b]
                podAffinity:
                  preferredDuringSchedulingIgnoredDuringExecution:
                    - weight: 50
                      podAffinityTerm:
                        topologyKey: topology.kubernetes.io/zone
                        labelSelector:
                          matchLabels:
                            app.kubernetes.io/part-of: checkout
                            app.kubernetes.io/component: cache
                podAntiAffinity:
                  preferredDuringSchedulingIgnoredDuringExecution:
                    - weight: 100
                      podAffinityTerm:
                        topologyKey: kubernetes.io/hostname
                        labelSelector:
                          matchLabels:
                            app.kubernetes.io/part-of: checkout
      enforce:
        action: allow
        workloads:
          targets:
            - pod
            - pod/containers
            - pod/initcontainers
            - pod/ephemeralcontainers
          # Allow any image source, but require the mutated pull policy.
          registries:
            - exp: '.*'
              policy: [Always]
          security:
            seccompProfiles:
              - types: [RuntimeDefault]
            appArmorProfiles:
              - types: [RuntimeDefault]
          classes:
            priority:
              - exact: [checkout-standard, checkout-batch]
          placement:
            nodeSelector:
              - key: {exact: [kubernetes.io/os]}
                values: {exact: [linux]}
              - key: {exact: [infrastructure.example.com/pool]}
                values: {exact: [shared]}
              - key: {exp: '^placement\.example\.com/[a-z0-9-]+$'}
                values: {exp: '^[a-z0-9-]+$'}
            tolerations:
              - key: {exact: [infrastructure.example.com/dedicated]}
                operators: [Equal]
                values: {exact: [shared]}
                effects: [NoSchedule]
              - key:
                  exact:
                    - node.kubernetes.io/not-ready
                    - node.kubernetes.io/unreachable
                operators: [Exists]
                effects: [NoExecute]
                tolerationSeconds:
                  max: 600
                  allowUnlimited: false
              - key: {exact: [node.kubernetes.io/memory-pressure]}
                operators: [Exists]
                effects: [NoSchedule]
            topologySpreadConstraints:
              - topologyKey:
                  exact: [topology.kubernetes.io/zone, kubernetes.io/hostname]
                whenUnsatisfiable: [DoNotSchedule, ScheduleAnyway]
                maxSkew: {min: 1, max: 3}
                labelSelector:
                  required: true
                  requirements:
                    - key: {exact: [app.kubernetes.io/part-of]}
                      operators: [In]
                      values: {exact: [checkout]}
            affinity:
              - types: [nodeAffinity]
                modes: [required]
                requirements:
                  - key: {exact: [topology.kubernetes.io/zone]}
                    operators: [In]
                    values: {exact: [zone-a, zone-b]}
              - types: [nodeAffinity]
                modes: [preferred]
                weight: {min: 1, max: 100}
                requirements:
                  - key: {exact: [kubernetes.io/arch]}
                    operators: [In]
                    values: {exact: [amd64, arm64]}
              - types: [podAffinity, podAntiAffinity]
                modes: [preferred]
                topologyKey:
                  exact: [topology.kubernetes.io/zone, kubernetes.io/hostname]
                namespaceScope: SameNamespace
                weight: {min: 1, max: 100}
                labelSelector:
                  required: true
                  requirements:
                    - key: {exact: [app.kubernetes.io/part-of]}
                      operators: [In]
                      values: {exact: [checkout]}
                    - key: {exact: [app.kubernetes.io/component]}
                      operators: [In, NotIn]
                      values: {exact: [api, worker, cache]}
Submitted value or scopeResult
Scheduler omitted or default-schedulerReplaced with tenant-scheduler.
Custom scheduler namePreserved; the placement and security mutations still apply.
Existing image pull secret referencesEach name is kept once in first-occurrence order; checkout-registry is appended if absent.
Image pull policy omitted, IfNotPresent, or NeverSet to Always on regular/init containers at creation and newly added ephemeral containers.
Root filesystem flag omitted or falseSet to true on regular/init containers at creation and newly added ephemeral containers.
Pod seccomp or AppArmor profile omittedDefaulted to RuntimeDefault.
Explicit Pod or container profilePreserved by mutation, then rejected unless its effective type is RuntimeDefault.
Priority class omitted, or the cluster’s global defaultSet to checkout-standard, with its priority and preemption policy.
Explicit priority classPreserved by mutation, then rejected unless it is checkout-standard or checkout-batch.
Existing node selector or required affinityCombined with the configured restrictions, then checked by the placement allowlists.
Namespace without example.com/application: checkoutThese rules do not apply.

Under enforce, the pod target validates placement, the priority class, and Pod-level profile defaults. The three container targets validate effective profiles and image pull policies for regular, init, and ephemeral containers, including container profile overrides. The registry matcher accepts all image sources while requiring Always. Under mutate, targets: [pod] selects all container groups for the root filesystem flag and image pull policy, and Pod-level fields, including image pull secrets, for the other properties; profile mutation still writes only Pod-level profiles. An Unconfined override or privileged container is rejected after mutation.

The toleration allowlist includes common Kubernetes-injected tolerations. Controllers such as DaemonSets may add others; include the entries needed by the workloads selected by your rule. The affinity validation uses one flat list for node affinity, Pod affinity, and Pod anti-affinity.

See the placement matcher reference for regular expressions, empty matchers, durations, selector operators, and namespace scope.

These mutations run on Pod creation. The root filesystem flag and image pull policy also apply to new ephemeral containers; the other properties are skipped on that subresource. Controller templates and existing containers are not rewritten. Profile enforcement also applies on subsequent Pod updates and pods/ephemeralcontainers updates. See Order and scope for rule composition and admission order.

2 - Ingress

Default or replace Ingress and Gateway classes and ListenerSet Gateway references

Ingress mutations configure Kubernetes Ingress and Gateway API resources under spec.rules[].mutate[].ingress. Use class mutations to supply class names and ListenerSet mutations to configure Gateway references in selected Tenant namespaces.

MutationResourceFieldOperations
types and classnetworking.k8s.io/v1 Ingress or gateway.networking.k8s.io/v1 Gatewayspec.ingressClassName or spec.gatewayClassNameMain-resource CREATE and UPDATE
listenerSets.parentRefgateway.networking.k8s.io/v1 ListenerSetspec.parentRefMain-resource CREATE and UPDATE

Gateway API v1 is the primary supported API. All Gateway API examples here use gateway.networking.k8s.io/v1. When installed CRDs also serve v1beta1 Gateways, those requests receive the same class mutation. ListenerSet parent reference mutation uses the v1 API.

Classes

Configure ingress.types together with ingress.class to set a class name:

FieldBehavior
typesRequired with class. Select Ingress, Gateway, or both.
classRequired with types. A non-empty class name, optionally using Tenant/Namespace rule templates.

For an Ingress, Capsule sets spec.ingressClassName. For a Gateway, it sets spec.gatewayClassName. These mutations apply on main-resource creation and updates. Deletes and status or other subresource updates are skipped.

The types field selects the resources for the class mutation. listenerSets.parentRef selects ListenerSets independently and does not use this list. Use separate entries to give Ingresses and Gateways different class names or mutation actions.

Default classes by namespace profile

This Tenant supplies different defaults to its private and public namespaces, then restricts the resulting class references with class enforcement:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          exposure: private
      mutate:
        - action: merge
          ingress:
            types: [Ingress, Gateway]
            class: internal
      enforce:
        action: allow
        ingress:
          types: [Ingress, Gateway]
          classes:
            - exact: [internal, custom]
    - namespaceSelector:
        matchLabels:
          exposure: public
      mutate:
        - ingress:
            types: [Ingress, Gateway]
            class: public
      enforce:
        action: allow
        ingress:
          types: [Ingress, Gateway]
          classes:
            - exact: [public]

In a private namespace, a class that is still absent or empty when mutation runs becomes internal; an explicit custom class is preserved and allowed. In a public namespace, a missing class becomes public. The IngressClass and GatewayClass objects are separate resources even when their names are the same. Create the required classes and configure their controllers separately; Capsule supplies the reference without creating or checking the class object.

Namespaces without either label receive no default from these rules, and other Tenants are outside their scope. An enforce.action applies only to enforcement; the mutation entry has its own independent action.

Merge and replace

ActionBehavior
merge (default)Fill an absent or empty class. Preserve a non-empty class supplied in the native field or the legacy Ingress annotation.
replaceSet the configured native class field on every matching write, overwriting an existing value. If the legacy Ingress annotation exists, update it to the same name.

Merge preserves values already supplied by Kubernetes or an earlier defaulting webhook. For example, a cluster default IngressClass already present in spec.ingressClassName is retained. If that preserved name is outside the configured class allow-list, admission is denied. With a legacy annotation alone, merge preserves the annotation and leaves an absent native class field unset.

To set a class regardless of an existing choice, use replace:

mutate:
  - action: replace
    ingress:
      types: [Ingress, Gateway]
      class: internal

Replacement synchronizes an existing kubernetes.io/ingress.class annotation; it does not add that annotation when it is absent. Every resulting class must still pass namespace rules, legacy Tenant class restrictions, and native API validation. A default alone does not restrict explicit choices; add class enforcement when required.

Templates, order, and conditions

Class names support Tenant/Namespace rule templates. For example:

mutate:
  - ingress:
      types: [Gateway]
      class: "{{ .tenant.metadata.name }}-edge"

The rendered class name must be a valid DNS subdomain. Configure types and class together; an empty class name is invalid.

Entries run in their effective order. The first matching merge supplies a missing class; later merges preserve it. A later replacement can override it. Use audience to select requesting subjects and mutation conditions to select individual resources. Conditions inspect the object after preceding mutations; a false condition skips only its entry. Conditions under enforce do not gate class mutation.

Rule and namespace changes

Rule changes and namespace label changes update the defaults used for subsequent admissions. They do not rewrite existing Ingresses or Gateways. Merge preserves a class already set on a later update; replace applies the current class on the next matching main-resource write.

ListenerSets

Install Gateway API CRDs and a Gateway controller supporting gateway.networking.k8s.io/v1 ListenerSets before using these examples. Other resource kinds, deletes, and status or other subresource updates do not apply this mutation. A ListenerSet has one spec.parentRef; Route resources use a separate spec.parentRefs list.

Parent reference

listenerSets.parentRef supplies a default Gateway reference for ListenerSets, or replaces that reference on every matching write.

Default a Gateway reference

Give namespaces within a Tenant different Gateway defaults with namespaceSelector. This example sends ListenerSets in namespaces labeled exposure: public to public-edge and those labeled exposure: private to private-edge:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: solar-owner
  rules:
    - namespaceSelector:
        matchLabels:
          exposure: public
      mutate:
        - ingress:
            listenerSets:
              parentRef:
                name: public-edge
                namespace: platform-gateways
    - namespaceSelector:
        matchLabels:
          exposure: private
      mutate:
        - ingress:
            listenerSets:
              parentRef:
                name: private-edge
                namespace: platform-gateways

action defaults to merge. Namespaces without either label receive no default from these rules. Namespaces in other Tenants are outside their scope.

In a solar-production namespace belonging to solar and labeled exposure: public, a tenant owner can submit a ListenerSet without a parent:

apiVersion: gateway.networking.k8s.io/v1
kind: ListenerSet
metadata:
  name: web
  namespace: solar-production
spec:
  listeners:
    - name: http
      hostname: web.solar.example.com
      port: 80
      protocol: HTTP

Capsule adds the following reference before validation and persistence:

spec:
  parentRef:
    group: gateway.networking.k8s.io
    kind: Gateway
    name: public-edge
    namespace: platform-gateways

An explicitly supplied parentRef is retained with merge. This is a default, so users can choose a different reference. Gateway attachment remains subject to attachment permissions.

Reference fields

Configure the complete parent reference under ingress.listenerSets.parentRef:

FieldRequiredBehavior
nameYesName of the parent Gateway. Must be a valid DNS subdomain.
namespaceNoNamespace of the parent. Omitted means the ListenerSet’s own namespace. An explicit empty string is invalid.
groupNoDefaults to gateway.networking.k8s.io.
kindNoDefaults to Gateway.

The group and kind defaults can also be written explicitly. An explicit empty group selects the core API group, following Gateway API reference semantics; it does not select the default Gateway group. The Gateway controller determines which parent kinds it supports.

Reference fields support the existing Tenant/Namespace rule templates. For example, set the Tenant label networking.example.com/default-gateway-namespace: platform-gateways and use this rule fragment to select a Gateway named after the Tenant:

mutate:
  - ingress:
      listenerSets:
        parentRef:
          name: "{{ .tenant.metadata.name }}-edge"
          namespace: '{{ index .tenant.metadata.labels "networking.example.com/default-gateway-namespace" }}'

Template source strings can contain up to 4096 characters, including expressions longer than a Kubernetes namespace name. Templates are rendered into the effective RuleStatus for each namespace. Rendered values must pass validation before the rule becomes ready: namespaces and kinds are limited to 63 characters, and names and API groups to 253 characters. Rendered namespaces must be valid DNS labels; the larger source limit does not relax that requirement.

Merge and replace

The action applies to the complete reference, including its namespace:

ActionBehavior
merge (default)Set spec.parentRef only when it is absent or null. Preserve any supplied reference, including one with an omitted namespace.
replaceReplace the entire spec.parentRef with the configured reference. An omitted namespace in the rule removes an existing namespace and selects the ListenerSet’s own namespace.

Merge does not fill individual fields in an existing reference. For example, parentRef: {name: custom-edge} still refers to a Gateway in the ListenerSet’s own namespace even if the rule supplies namespace: platform-gateways. An invalid supplied reference remains subject to Gateway API validation; merge does not repair it or fall back when a referenced Gateway is missing.

Omitting parentRef in the rule, or setting it to null, supplies no mutation. An empty parentRef: {} is invalid because name is required. An entry must supply at least one mutation property.

To set the same reference on every matching creation or update, use replace:

mutate:
  - action: replace
    ingress:
      listenerSets:
        parentRef:
          name: public-edge
          namespace: platform-gateways

Order and conditions

Applicable rules retain their effective order, and mutation entries run in list order. The first matching merge fills a missing reference; later merges preserve it. A later replacement can override it. Put specific merge defaults before a shared fallback when their selectors overlap.

Use audience to select requesting subjects and mutation conditions to select individual ListenerSets. This fragment supplies a private Gateway for objects labeled exposure: private, then a public fallback for other objects without a parent:

mutate:
  - conditions:
      - name: private-listener
        expression: >-
          has(object.metadata.labels) &&
          'exposure' in object.metadata.labels &&
          object.metadata.labels.exposure == 'private'          
    ingress:
      listenerSets:
        parentRef:
          name: private-edge
          namespace: platform-gateways
  - ingress:
      listenerSets:
        parentRef:
          name: public-edge
          namespace: platform-gateways

Conditions inspect the object after preceding mutations. A false condition skips only its entry. Workload-only mutation conditions are not evaluated for ListenerSet requests. Conditions under enforce do not control these mutations, and enforce.action does not select or disable them.

Attachment permissions

Capsule supplies the reference without reading or creating the Gateway. It does not check that the parent exists or grant attachment permissions. The Gateway’s spec.allowedListeners must permit ListenerSets from the selected namespaces, including when the Gateway is in a platform namespace outside the Tenant.

For a managed Gateway and tenant-owned listeners, see Envoy Gateway: Tenant ListenerSets. Combine this mutation with ingress hostname enforcement to restrict listener hostnames; mutated resources still pass through enforcement. Metadata rules can also apply to the same ListenerSets: select apiGroups: [gateway.networking.k8s.io/v1] and kinds: [ListenerSet] in the metadata rule.

Rule and namespace changes

Rule and namespace label changes update the effective profile for subsequent admissions. They do not change the parent of existing ListenerSets. With merge, a ListenerSet that already has a parent keeps it on later updates, even after its default changes. If the parent is removed on an update, the currently applicable default is inserted. With replace, the reference is updated on the next matching main-resource write.

3 - Devices

Default DeviceClass references in ResourceClaims and ResourceClaimTemplates.

Device mutations configure DRA class defaults under spec.rules[].mutate[].devices.class. They fill missing class names when a ResourceClaim or ResourceClaimTemplate is created in a selected Tenant namespace.

MutationResourcesOperations
devices.classresource.k8s.io/v1 and resource.k8s.io/v1beta2 ResourceClaims and ResourceClaimTemplatesMain-resource CREATE only.

Use namespace selection to give namespaces within one Tenant different defaults. Audience and mutation conditions determine which entries apply. Defaults remain subject to DeviceClass enforcement.

Default class

devices.class supplies a default for every existing exactly request and every firstAvailable alternative whose deviceClassName is omitted, null, or the empty string. Explicit non-empty class names are always retained.

This example defaults GPU requests in namespaces labeled device-profile: gpu and FPGA requests in namespaces labeled device-profile: fpga. The accompanying enforcement rules limit each profile to its permitted classes:

apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  owners:
    - kind: User
      name: alice
  rules:
    - namespaceSelector:
        matchLabels:
          device-profile: gpu
      mutate:
        - action: merge
          devices:
            class: "{{ .tenant.metadata.name }}-gpu"
      enforce:
        action: allow
        devices:
          classes:
            - exact:
                - solar-gpu
                - solar-gpu-h100
    - namespaceSelector:
        matchLabels:
          device-profile: fpga
      mutate:
        - devices:
            class: solar-fpga
      enforce:
        action: allow
        devices:
          classes:
            - exact:
                - solar-fpga

The first default uses the shared Tenant/Namespace templates support and resolves to solar-gpu. Class values must resolve to valid DeviceClass names. Source values can contain up to 4096 characters to allow templates; rendered names must satisfy Kubernetes DNS subdomain and length requirements.

Namespaces without either profile label receive no default or class restriction from these rules. Keep profile labels under platform control, and add a broader deny rule if other namespaces must not request devices. The enforcement reference shows that pattern. Namespaces in other Tenants use their own profiles.

In a solar-production namespace belonging to solar and labeled device-profile: gpu, a tenant owner can submit this claim:

apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: accelerator
  namespace: solar-production
spec:
  devices:
    requests:
      - name: gpu
        exactly: {}

Capsule supplies spec.devices.requests[0].exactly.deviceClassName: solar-gpu. For a ResourceClaimTemplate, place the same requests under spec.spec.devices.requests.

Merge behavior and order

merge is the only supported action for devices.class, and is the default when action is omitted. A mutation entry supplying devices.class with action: replace is rejected. Keep it in a separate entry from other properties that require replacement.

Incoming value or structureResult
Class name omitted, null, or "" inside an existing requestFilled with the first applicable class default.
Explicit non-empty class namePreserved, even when it differs from the default. Class enforcement can still reject it.
Mixture of empty and explicit names in firstAvailableEmpty names are filled independently; explicit alternatives and their order are preserved.
Missing device requests, or a request with neither exactly nor firstAvailableNo request structure is created. Kubernetes validation still applies.
Omitted or null devices.classSupplies no default. An entry must supply some mutation property to be valid.
Empty devices: {}Supplies no change and is invalid as the only property in a mutation entry.
Configured class value "" or an invalid class nameInvalid configuration.

Entries run in declaration order. Once a default fills a name, later defaults retain that value. The first applicable default wins for each empty field. Place more specific defaults before broader fallback defaults.

Conditions see the resource after metadata mutation and preceding device defaults. A false condition skips the entry. A caller excluded by an audience receives no default from that entry. With no audience, defaults apply to every caller, including controllers; all explicit class names remain unchanged.

Scope and enforcement

Defaulting neither creates a DeviceClass nor verifies that it exists. Provision the classes and DRA driver needed for allocation. Selector-based enforcement and an existing Tenant.spec.deviceClasses restriction may additionally require every class to exist at admission.

A default is not an access grant. The resulting names must pass all applicable class restrictions, including every fallback and the existing Tenant-wide setting. With the GPU profile above, an explicit other-gpu is preserved by mutation and rejected by the allow-list. A default outside that allow-list is rejected as well.

Defaulting runs only on creation. Metadata updates, status updates, and deletion do not change class references. Updating a rule or namespace profile affects subsequent creations; it does not reconcile existing claims or templates.

Kubernetes controllers create claims from the class names already stored in a template. If a default changes after template creation, those explicit template names are retained on the generated claim and checked against the current enforcement rules. A scheduler-generated claim also keeps its explicit class names, preserving the inputs used for allocation selection.

Device defaulting does not grant administrative device access. Keep the DRA administrative access baseline in place separately.