Managed Resources and Server-Side Apply

Ownership, server-side apply, resource policies, and status for TenantResource and GlobalTenantResource replications.

Capsule replications use a shared resource manager to apply Kubernetes objects, track their ownership, and clean them up when they leave scope. Both TenantResource and GlobalTenantResource configure this behavior through spec.resources[].policy.

Each resource block has its own policy, which applies to every destination produced by its rawItems, namespacedItems, and generators. Use separate blocks when different resources or namespaces need different ownership, protection, or cleanup behavior.

Apply and ownership model

Capsule uses Kubernetes server-side apply (SSA) to manage the fields declared by a replication. Stable field managers distinguish the replication and destination, so different replications can contribute fields to the same object. Inspect the destination’s metadata.managedFields to see which manager owns each field:

kubectl get configmap namespace-profile -n solar-uat -o json \
  | jq '.metadata.managedFields[] | {manager, operation, fieldsV1}'

SSA allows Capsule and another manager to own different fields on the same object. Changing a field owned by another manager causes an apply conflict unless Capsule is configured to force ownership.

During reconciliation, Capsule gathers source objects, renders templates, and applies the resulting resources to the selected destinations. Changes to the replication trigger reconciliation, and spec.resyncPeriod controls periodic refreshes. Update the replication’s source objects, templates, or policy to change managed content. Fields previously owned by Capsule can be removed when omitted from a later apply; include them in subsequent rendered resources when they must be retained.

Resource policy

This GlobalTenantResource distributes a ConfigMap to production namespaces of Tenants labelled energy: renewable. Its policy applies to each replicated ConfigMap:

apiVersion: capsule.clastix.io/v1beta2
kind: GlobalTenantResource
metadata:
  name: namespace-profile
spec:
  tenantSelector:
    matchLabels:
      energy: renewable
  resyncPeriod: 60s
  resources:
    - namespaceSelector:
        matchLabels:
          environment: production
      policy:
        creation: Owner
        protect: true
        force: false
        deletion: Remove
      rawItems:
        - apiVersion: v1
          kind: ConfigMap
          metadata:
            name: namespace-profile
          data:
            environment: production

The same resource block can be used in a TenantResource to distribute the ConfigMap within a single Tenant.

FieldValuesDefaultBehavior
creationOwner, MergeOwnerWhether an existing object may be adopted
protecttrue, falsetrueWhether admission blocks direct changes while managed
forcetrue, falsefalseWhether SSA may take conflicting field ownership
deletionRemove, OrphanRemoveWhat happens when the parent stops managing the object
conditionCEL boolean expressionOmittedWhether to apply rendered content to each destination

Owner creates an absent target and manages objects created by the applying controller. An unrelated existing object with the same identity causes an ownership error. Merge adopts an existing object when possible and creates it otherwise. Use Merge when the replication should manage fields on existing objects.

force: true resolves SSA conflicts by taking ownership of the rendered fields. It does not bypass Kubernetes RBAC, admission, or schema validation, and can displace another controller’s ownership.

With protect: true, Capsule marks the object as protected. The generic Capsule admission webhook rejects direct updates and deletion while the parent manages it, except for the controller and the resolved execution ServiceAccount. Disable protection only when another actor must modify the object during its managed lifetime.

With deletion: Remove, when multiple Capsule resource managers share an adopted target, removing one manager preserves the shared tracking and protection metadata while another Capsule resource manager remains. The final departing manager removes that metadata without deleting fields owned by external managers.

Cleanup depends on both the deletion policy and whether Capsule created or adopted the object:

PolicyObject created by CapsuleExisting object adopted with Merge
RemoveDelete the objectRelinquish managed fields and tracking
OrphanKeep the object and remove tracking/protectionKeep the object and applied fields, then remove tracking/protection

Cleanup runs when a target leaves the desired replication output, including namespace or Tenant deselection, resource block removal, or deletion of the replication. It uses the last successfully reconciled policy recorded for that target. A false apply condition does not prevent cleanup.

Orphan leaves configuration behind after the replication stops managing it. Choose it when those resources must survive independently of the replication. For existing manifests using spec.settings or spec.pruningOnDelete, see the policy migration guide.

Apply conditions

Set spec.resources[].policy.condition on a TenantResource or GlobalTenantResource to decide when rendered content is applied. One expression is evaluated independently for every destination produced by that resource block; separate blocks can use different conditions. The expression must return a boolean and may contain up to 4096 characters. Omit the field to keep unconditional apply; a blank expression is invalid.

InputMeaning
objectThe existing destination resource, read using the replication’s execution identity; null if it does not exist. It is not the rendered candidate.
nowThe UTC timestamp at evaluation time. Use timestamp(...) and duration(...) for time comparisons.

Apply conditions use CEL directly. There is no self, .self, Go-template expansion, or access to template context in this expression. Load context and render the desired resource through generators. Rendering happens before condition evaluation, so a condition cannot suppress context-loading or template errors.

For example, create a target only when it is absent:

policy:
  condition: "object == null"

To initialize a target and then allow a refresh after five minutes:

policy:
  condition: |
    object == null ||
    !has(object.metadata.annotations) ||
    !('keys.example.org/rotated-at' in object.metadata.annotations) ||
    now >= timestamp(object.metadata.annotations['keys.example.org/rotated-at']) + duration('5m')    

The generator must write the keys.example.org/rotated-at annotation on each successful refresh. Guard missing objects and fields before accessing them; use bracket notation for annotation keys containing dots or slashes. A missing timestamp initializes this example, while a malformed timestamp causes an evaluation error. See the complete rotation example for the template, retention behavior, and verification steps.

OutcomeApply behavior
Condition omittedExisting unconditional apply behavior.
trueApply using the block’s creation, force, and protection policies; normal authorization and ownership checks still apply.
falseSkip rendered content and preserve its last-apply timestamp. Reconcile protection and retain the current policy for already managed targets. A never-applied target is not adopted, protected, or modified.
Invalid syntax or non-boolean result typeAdmission rejects the expression at spec.resources[i].policy.condition.
Evaluation or target-read errorThe target is not written; the failure is reported through existing status. A forbidden read is not treated as an absent object.

There is no configurable action for a false result. Conditions do not provide an all-or-nothing transaction across targets: each target is evaluated separately. They also do not change SSA retention. To preserve old fields owned by Capsule, include them in subsequent rendered resources, as the age-key example does.

For example, changing protect: true to protect: false removes protection on an already managed target even while its condition is false, provided no other Capsule resource manager still shares the target. Protection is also reconciled when the condition becomes true or is removed; protection established during a skipped apply does not persist after a sole manager disables it. Changing deletion: Remove to deletion: Orphan takes effect for subsequent cleanup. creation and force govern the next permitted content apply; changing them does not override a false condition. Rendered labels, annotations, and data remain unchanged, including key material and rotation timestamps. Capsule may patch its own protection metadata, so the target’s resource version can change. When that metadata already matches, a skipped reconciliation performs no target write. Policy changes that fail to reconcile are reported through existing status.

Evaluation lifecycle

Both replication APIs render and evaluate conditions during reconciliation, including periodic resync. This makes conditions suitable for recurring namespace configuration and key rotation. Time passing alone does not trigger an apply; the condition is checked the next time the replication reconciles. Choose a spec.resyncPeriod that suits the required refresh interval.

Scope, tracking, and cleanup

For namespaced targets in the default Namespace scope, Capsule uses the selected destination Namespace, which can differ from the Namespace containing a TenantResource. A TenantResource is restricted to namespaced resources within its own Tenant. A GlobalTenantResource can also manage cluster-scoped resources and use spec.scope to control whether resources are rendered per Namespace, per Tenant, or once. See replication scope for those options. Cluster-scoped resources are applied without a namespace.

Capsule records processed targets in the replication’s .status.processedItems, including whether they were created or adopted and the policy used for cleanup. Successful targets remain recorded if a later target fails, so subsequent reconciliation can retry failures and clean up resources that leave scope.

Replicated objects may carry tracking labels such as:

metadata:
  labels:
    projectcapsule.dev/created-by: resources
    projectcapsule.dev/managed-by: resources

created-by identifies resources Capsule created; managed-by identifies resources under management, including adopted objects. Capsule sets tracking metadata independently of the rendered manifest, so a template cannot forge whether an object was created or adopted. Use processed-item status to inspect a particular replication’s relationship to a target.

Status and failures

Both replication APIs report their managed resources through .status.processedItems. For example, the namespace-profile GlobalTenantResource can report successful ConfigMap replication into two namespaces of the solar Tenant:

status:
  size: 2
  selectedTenants:
    - solar
  processedItems:
    - version: v1
      kind: ConfigMap
      namespace: solar-uat
      name: namespace-profile
      tenant: solar
      status:
        clusterScoped: false
        created: true
        lastApply: "2026-08-31T10:02:13Z"
        type: Ready
        status: "True"
        policy:
          creation: Owner
          protect: true
          force: false
          deletion: Remove
    - version: v1
      kind: ConfigMap
      namespace: solar-prod
      name: namespace-profile
      tenant: solar
      status:
        clusterScoped: false
        created: true
        lastApply: "2026-08-31T10:02:14Z"
        type: Ready
        status: "True"
        policy:
          creation: Owner
          protect: true
          force: false
          deletion: Remove

Each item identifies the target’s API group, version, kind, name, namespace, Tenant, and scope, whether it was created or adopted, its last apply time, and its latest condition or error. Skipped targets report ConditionNotMet: apply skipped. A previously applied target keeps its last successful apply time; a never-applied target has none. The retained policy records the last successful policy reconciliation and determines subsequent cleanup; the condition expression is evaluated from the source block and is not stored in that snapshot. Ownership violations, SSA conflicts, RBAC denials, unavailable resource kinds, invalid manifests, and admission failures are reported through processed-item status and the replication’s conditions. .status.serviceAccount identifies the ServiceAccount used to execute the replication.

kubectl get globaltenantresource namespace-profile -o json \
  | jq '.status | {size, selectedTenants, serviceAccount, processedItems, conditions}'

kubectl get tenantresource namespace-profile -n solar-system -o json \
  | jq '.status | {size, serviceAccount, processedItems, conditions}'

To recover from a failure, correct the source object, permissions, template, or policy and let reconciliation retry. Update the replication rather than editing its processed-item status or making direct changes to protected destination objects.

Last modified October 21, 2026: chore: merge release branch (#165) (69f84ec)