# TenantResources

> Replicate resources across a Tenant's Namespaces as Tenant Owner

> For the complete documentation index, see [llms.txt](https://projectcapsule.dev/llms.txt).
> The HTML version of this page is at https://projectcapsule.dev/docs/replications/tenant/.

## Overview

`TenantResource` is a namespace-scoped CRD that lets Tenant owners automatically replicate Kubernetes resources across all Namespaces in their Tenant - without manual distribution or custom automation. It is the tenant-level counterpart to [GlobalTenantResource](/docs/replications/global/), which is reserved for cluster administrators.

The diagram below shows that an Administrator or a Tenant Owner can create a `TenantResource` inside a `Tenant`. In the `TenantResource` spec, a user specifies which resource they would like to replicate across the `Tenant`. When applied, this resource gets automatically distributed across all Namespaces that are part of the `Tenant`.

![Tenant Resource Replication overview](/images/content/replication-tenantresource.png)


> **OpenShift: etcd Encryption**
>
> If you are running on OpenShift with etcd encryption enabled and replicating <code>ConfigMap</code>s or <code>Secret</code>s, you must exclude the OpenShift storage version migrator from the replication webhook. Without this, the migrator cannot rotate encryption keys. See the <a href="/docs/operating/setup/openshift/#etcd-encryption">OpenShift installation guide</a> for the required configuration.


## Prerequisites

Tenant owners must have RBAC permission to create, update, and delete `TenantResource` objects. The following `ClusterRole` aggregates to the `admin` role, granting all holders permission to manage `TenantResource` instances:

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: allow-tenant-resources
  labels:
    rbac.authorization.k8s.io/aggregate-to-admin: "true"
rules:
- apiGroups: ["capsule.clastix.io"]
  resources: ["tenantresources"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
```

## Example

Alice, the project lead for the `solar` tenant, wants to provision a PostgreSQL database for each production Namespace automatically:

```bash
$ kubectl get namespaces -l capsule.clastix.io/tenant=solar --show-labels
NAME           STATUS   AGE   LABELS
solar-1        Active   59s   capsule.clastix.io/tenant=solar,environment=production,kubernetes.io/metadata.name=solar-1,name=solar-1
solar-2        Active   58s   capsule.clastix.io/tenant=solar,environment=production,kubernetes.io/metadata.name=solar-2,name=solar-2
solar-system   Active   62s   capsule.clastix.io/tenant=solar,kubernetes.io/metadata.name=solar-system,name=solar-system
```

She creates a `TenantResource` in `solar-system`:

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: solar-db
  namespace: solar-system
spec:
  resyncPeriod: 60s
  resources:
    - additionalMetadata:
        labels:
          "replicated-by": "capsule"
      namespaceSelector:
        matchLabels:
          environment: production
      rawItems:
        - apiVersion: postgresql.cnpg.io/v1
          kind: Cluster
          metadata:
            name: "postgres-{{namespace}}"
          spec:
            description: PostgreSQL cluster for the {{tenant.name}} Project
            instances: 3
            postgresql:
              pg_hba:
                - hostssl app all all cert
            primaryUpdateStrategy: unsupervised
            storage:
              size: 1Gi
```

Capsule replicates the `Cluster` resource into every Namespace matching the `namespaceSelector`. The Namespace where the `TenantResource` itself lives (`solar-system`) is automatically excluded, and Capsule injects labels to prevent the `TenantResource` from propagating into unowned Namespaces.

```bash
$ kubectl get clusters.postgresql.cnpg.io -A
NAMESPACE   NAME              AGE   INSTANCES   READY   STATUS                     PRIMARY
solar-1     postgres-solar-1  80s   3           3       Cluster in healthy state   postgresql-1
solar-2     postgres-solar-2  80s   3           3       Cluster in healthy state   postgresql-1
```

> Objects managed by this controller can be either **created** (new objects) or **adopted** (existing objects). See [Object Management](#object-management) in the Advanced section for full details.

---

## Basic Usage

### Resources

Each block accepts a [resource policy](#object-management) for creation, protection,
SSA conflicts, cleanup, and optional CEL conditions.

A resource block defines *what* to replicate. Multiple blocks can be stacked in the `resources` array, each using one or more of the strategies below.

A `TenantResource` can only create namespaced resources in the Namespaces of its Tenant. Cluster-scoped kinds (such as `ClusterRole` or `Namespace`) are not supported in `rawItems`, `generators` or `namespacedItems`. Cluster administrators can use a [GlobalTenantResource](/docs/replications/global/) for those.

#### NamespaceSelector

The `namespaceSelector` field restricts replication to Namespaces matching a label selector. Capsule also protects selected resources from modification by Tenant users via its webhook.

#### AdditionalMetadata

Use `additionalMetadata` to attach extra `labels` and `annotations` to every generated object. [Fast Template values](/docs/operating/concepts/templating/#fast-templates) are supported:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-namespace-rbac
spec:
  resources:
    - additionalMetadata:
        labels:
          k8s.company.com/tenant: "{{tenant.name}}"
        annotations:
          k8s.company.com/cost-center: "inv-120"
      generators:
        - missingKey: error
          template: |
            ---
            apiVersion: rbac.authorization.k8s.io/v1
            kind: Role
            metadata:
              name: configmap-{{$.tenant.metadata.name}}-reader
              labels:
                k8s.company.com/tenant: "test"
            rules:
              - apiGroups: [""]
                verbs: ["get", "list"]
                resources: ["configmaps"]
```

When the same label key appears in both `additionalMetadata` and the template, `additionalMetadata` takes priority.

The following labels are always stripped because they are reserved for the controller:

  * `capsule.clastix.io/resources`
  * `projectcapsule.dev/created-by`
  * `capsule.clastix.io/managed-by`
  * `projectcapsule.dev/managed-by`

#### NamespacedItems

Reference existing resources for replication across Tenant Namespaces. The controller validates that any resource kind listed here is namespace-scoped; cluster-scoped kinds are rejected with an error.

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
spec:
  resyncPeriod: 60s
  resources:
    - namespacedItems:

      # Replicate all Configmaps labeled with projectcapsule.dev/replicate: "true"
      - apiVersion: v1
        kind: ConfigMap
        selector:
          matchLabels:
            projectcapsule.dev/replicate: "true"

      # Replicate all Configmaps labeled with projectcapsule.dev/replicate: "true" and in namespace capsule-system
      - apiVersion: v1
        kind: ConfigMap
        namespace: capsule-system
        selector:
          matchLabels:
            projectcapsule.dev/replicate: "true"

      # Replicate Configmap named "logging-config" in namespace capsule-system labeled with projectcapsule.dev/replicate: "true" and in namespace capsule-system
      - apiVersion: v1
        kind: ConfigMap
        name: logging-config
        namespace: capsule-system
```

**Note**: Resources with the label `projectcapsule.dev/created-by: resources` are ignored by `namespacedItems` to prevent reconciliation loops.

If you try to define a cluster-scoped resource under `namespacedItems`, the reconciliation will fail immediately:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
spec:
  resyncPeriod: 60s
  resources:
    - namespacedItems:
      - apiVersion: addons.projectcapsule.dev/v1alpha1
        kind: SopsProvider
        name: infrastructure-provider
        optional: true

status:
  conditions:
  - lastTransitionTime: "2026-01-15T21:04:15Z"
    message: cluster-scoped kind addons.projectcapsule.dev/v1alpha1/SopsProvider is
      not allowed
    reason: Failed
    status: "False"
    type: Ready
```

##### Name

Providing `name` triggers a `GET` request for that single resource rather than a `LIST`:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
  namespace: wind-test
spec:
  resyncPeriod: 60s
  resources:
    - namespacedItems:
      # Fetch ConfigMaps labeled with the tenant name and replicate them into each Tenant Namespace
      - apiVersion: v1
        kind: ConfigMap
        name: "logging-config"
```

This distributes the `ConfigMap` named `logging-config` to all other Namespaces of the Tenant that `wind-test` belongs to.

[Fast Templates](/docs/operating/concepts/templating/#fast-templates) are supported for `name`, `namespace`, and `selector`.

##### Namespace

Providing only `namespace` performs a `LIST` of all resources of that kind in that namespace:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
spec:
  resources:
  - namespacedItems:
    - apiVersion: v1
      kind: ConfigMap
      name: config-namespace
      optional: true
```

[Fast Templates](/docs/operating/concepts/templating/#fast-templates) are supported for the `namespace` property:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
spec:
  resyncPeriod: 60s
  resources:
    - namespacedItems:
      # Fetch ConfigMaps labeled with the tenant name and replicate them into each Tenant Namespace
      - apiVersion: v1
        kind: Secret
        namespace: "{{tenant.name}}-system"
```

##### Selector

When using `selector`, the selector labels are stripped from the replicated objects. This prevents the replicated copy from also matching the source selector, which would cause a circular reconciliation loop.

Source `ConfigMap`:

```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  labels:
    projectcapsule.dev/replicate: "true"
  namespace: wind-test
data:
  player_initial_lives: "3"
  ui_properties_file_name: "user-interface.properties"
```

`TenantResource`:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: app-config
spec:
  resources:
    - namespacedItems:
      - apiVersion: v1
        kind: ConfigMap
        selector:
          matchLabels:
            projectcapsule.dev/replicate: "true"
```

Resulting object in `wind-prod` (notice the absence of `projectcapsule.dev/replicate`):

```yaml
apiVersion: v1
data:
  player_initial_lives: "3"
  ui_properties_file_name: "user-interface.properties"
kind: ConfigMap
metadata:
  labels:
    projectcapsule.dev/created-by: resources
    projectcapsule.dev/managed-by: resources
  name: app-config
  namespace: wind-prod
  resourceVersion: "784529"
  uid: 5f10a3f3-863e-4f45-9454-cff8f5bce86a
```

[Fast Templates](/docs/operating/concepts/templating/#fast-templates) are supported for `selector`:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
spec:
  resyncPeriod: 60s
  resources:
    - namespacedItems:
      # Fetch ConfigMaps labeled with the tenant name and replicate them into each Tenant Namespace
      - apiVersion: v1
        kind: ConfigMap
        selector:
          matchLabels:
            company.com/replicate-for: "{{tenant.name}}"
```

#### Raw

Raw items let you define resources inline as standard Kubernetes manifests. Use this when the resource does not yet exist in the cluster, or when you want to define it directly in the spec. [Fast Templates](/docs/operating/concepts/templating/#fast-templates) are supported.

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
spec:
  resyncPeriod: 300s
  resources:
    - rawItems:
      - apiVersion: v1
        kind: LimitRange
        metadata:
          name: "{{tenant.name}}-{{namespace}}-resource-constraint"
        spec:
          limits:
          - default: # this section defines default limits
              cpu: 500m
            defaultRequest: # this section defines default requests
              cpu: 500m
            max: # max and min define the limit range
              cpu: "1"
            min:
              cpu: 100m
            type: Container
```

The following example creates a `NetworkPolicy` in each Namespace of the Tenant:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-network-policies
spec:
  resyncPeriod: 600s
  resources:
    - rawItems:
        - apiVersion: networking.k8s.io/v1
          kind: NetworkPolicy
          metadata:
            name: allow-same-tenant
          spec:
            podSelector: {}
            policyTypes:
              - Ingress
            ingress:
              - from:
                  - namespaceSelector:
                      matchLabels:
                        capsule.clastix.io/tenant: "{{tenant.name}}"
```

Because [Server-Side Apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/) is used, you only need to specify the fields you want to manage - the full resource spec is not required.

For more advanced templating, consider [Generators](#generators).

#### Generators

Generators render one or more Kubernetes objects from a Go template string. The template content must be valid YAML; multi-document output separated by `---` is supported. The template engine is based on [go-sprout](https://github.com/go-sprout/sprout) - see [available functions](/docs/operating/concepts/templating/#sprout-templating).

A simple example that creates a `Role` in each Namespace of the Tenant:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-namespace-rbac
spec:
  resources:
    - generators:
        - missingKey: error
          template: |
            apiVersion: rbac.authorization.k8s.io/v1
            kind: Role
            metadata:
              name: {{$.tenant.metadata.name}}-secret-reader
            rules:
            - apiGroups: [""]
              resources: ["secrets"]
              verbs: ["get", "watch", "list"]
```

Templates can also use flow control. The following example reads the free-form `spec.data` of the `Tenant` and renders each entry into a `ConfigMap` in every Namespace of the Tenant:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
  name: solar
spec:
  data:
    costCenter: inv-120
    environment: production
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-data
  namespace: solar-system
spec:
  resources:
    - generators:
        - missingKey: zero
          template: |
            ---
            apiVersion: v1
            kind: ConfigMap
            metadata:
              name: {{ $.tenant.metadata.name }}-data
            data:
            {{- range $key, $value := $.tenant.spec.data }}
              {{ $key }}: "{{ $value }}"
            {{- end }}
```

Which renders in each Namespace:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: solar-data
data:
  costCenter: "inv-120"
  environment: "production"
```

See [Base Context](#base-context) for available template variables. To load additional resources into the template context, see [Context](#context) in the Advanced section.

##### Template Snippets

Some snippets that might be useful for certain cases.

###### Names

Extract the `Tenant` name:

```html
{{ $.tenant.metadata.name }}
```

Extract the `Namespace` name:

```html
{{ $.namespace.metadata.name }}
```

###### Foreach Owner

Iterate over all owners of a Tenant:

```html
  {{- range $.tenant.status.owners }}
    {{ .kind }}: {{ .name }}
  {{- end }}
```

##### MissingKey

Controls template behaviour when a referenced context key is absent.

###### Invalid

Continues execution silently. Missing keys render as the string `"<no value>"`.

This definition with the missing context:

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: missing-key
spec:
  resources:
  - generators:
    - missingKey: invalid
      template: |
        ---
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: show-key
        data:
          value: {{ $.custom.account.name }}
```

Turns into after templating:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: show-key
data:
  value: "<no value>"
```

###### Zero

**This is the default behavior.** Missing keys resolve to the zero value of their type (usually an empty string).

This definition with the missing context:

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: missing-key
spec:
  resources:
  - generators:
    - missingKey: zero
      template: |
        ---
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: show-key
        data:
          value: {{ $.custom.account.name }}
```

Turns into after templating:

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: show-key
data:
  value: ""
```

###### Error

Stops execution immediately with an error when a required key is missing.

This definition with the missing context:

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: missing-key
spec:
  resources:
  - generators:
    - missingKey: error
      template: |
        ---
        apiVersion: v1
        kind: ConfigMap
        metadata:
          name: show-key
        data:
          value: {{ $.custom.account.name }}
```

Will error the `TenantResources`:

```sh
NAME                    ITEMS   READY   STATUS                                                                                                               AGE
missing-key   6       False   error running generator: template: tpl:7:13: executing "tpl" at <$.custom.account.name>: map has no entry for key "custom"   9m5s
```

---

### Reconciliation

#### Period

`TenantResources` reconcile on the interval defined by `resyncPeriod`. The default is `60s`. Capsule does not watch source resources for changes; it reconciles periodically. A very short interval on large clusters with many Tenants and Namespaces can cause performance issues - tune this value accordingly.

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: renewable-pull-secrets
  namespace: wind-test
spec:
  resyncPeriod: 300s # 5 minutes
  resources:
    - namespacedItems:
        - apiVersion: v1
          kind: Secret
          namespace: harbor-system
          selector:
            matchLabels:
              tenant: renewable
```

#### Manual

To trigger an immediate reconciliation, add the `reconcile.projectcapsule.dev/requestedAt` annotation. The annotation is removed once reconciliation completes, making the process repeatable.

```bash
kubectl annotate tenantresource renewable-pull-secrets -n wind-test \
  reconcile.projectcapsule.dev/requestedAt="$(date -Iseconds)"
```

---

### Impersonation

Replication runs as the [`default` ServiceAccount](#default-serviceaccount) of the `TenantResource` Namespace. Impersonation gives the operations a proper audit trail and limits privilege exposure. You can check which ServiceAccount is currently in use via the object's status:

```bash
kubectl get tenantresource custom-cm -n wind-test -o jsonpath='{.status.serviceAccount}' | jq
{
  "name": "default",
  "namespace": "wind-test"
}
```

To use a different ServiceAccount, set the `serviceAccount` field on the object. For `TenantResource`, only the name is required - the namespace is always inferred from the Namespace the `TenantResource` resides in:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-resource-replications
  namespace: wind-test
spec:
  serviceAccount:
    name: "tenant-replicator"
  resources:
    - namespacedItems:
      - apiVersion: v1
        kind: ConfigMap
        name: "config-namespace"
```

If the ServiceAccount lacks the required RBAC, replication will fail with a permission error:

```
  - kind: ConfigMap
    name: game-demo
    namespace: wind-test
    status:
      created: true
      message: 'apply failed for item 0/raw-0: applying object failed: configmaps
        "game-demo" is forbidden: User "system:serviceaccount:wind-test:tenant-replicator"
        cannot patch resource "configmaps" in API group "" in the namespace "wind-test"'
      status: "False"
      type: Ready
    tenant: wind
    version: v1
```

Grant the ServiceAccount the necessary permissions:

```yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: capsule-tenant-replications
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["list", "get", "patch", "create", "delete"]
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-replicator-rbac
spec:
  resyncPeriod: 60s
  resources:
    - rawItems:
      - apiVersion: rbac.authorization.k8s.io/v1
        kind: RoleBinding
        metadata:
          name: wind-replication
        subjects:
          - kind: ServiceAccount
            name: tenant-replicator
            namespace: wind-test
        roleRef:
          kind: ClusterRole
          name: capsule-tenant-replications
          apiGroup: rbac.authorization.k8s.io
```

#### Required Permissions

The following permissions are required for each resource type managed by the replication feature:

  * `get` (always required)
  * `create` (always required)
  * `patch` (always required)
  * `delete` (always required)
  * `list` (required for [Namespaced Items](#namespaceditems) and [Context](#context))

Missing any of these will cause replication to fail.

#### Default ServiceAccount

`tenantDefaultServiceAccount` gives every `TenantResource` a controlled identity and is set to `default`. Per-object `serviceAccount` fields override it. Only the name is required; the namespace is always the one the `TenantResource` resides in.

[Read more about Impersonation](/docs/operating/setup/configuration/#impersonation).

```yaml
manager:
  options:
    impersonation:
      tenantDefaultServiceAccount: "default"
```

The default ServiceAccount must have sufficient RBAC. You can use a [GlobalTenantResource](/docs/replications/global/) to distribute the required `RoleBinding` across all Tenants:

```yaml
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: capsule-default-namespace
rules:
- apiGroups: [""]
  resources: ["limitranges", "secrets"]
  verbs: ["get", "patch", "create", "delete", "list"]
---
apiVersion: capsule.clastix.io/v1beta2
kind: GlobalTenantResource
metadata:
  name: default-sa-replication
spec:
  resyncPeriod: 60s
  resources:
    - rawItems:
      - apiVersion: rbac.authorization.k8s.io/v1
        kind: RoleBinding
        metadata:
          name: default-replication
        subjects:
          - kind: ServiceAccount
            name: default
            namespace: wind-test
        roleRef:
          kind: ClusterRole
          name: capsule-tenant-replications
          apiGroup: rbac.authorization.k8s.io
```

---

## Advanced

This section covers more advanced features of the Replication setup.

### Object Management

Set `spec.resources[].policy` separately for each resource block. The policy applies
to all destinations produced by that block, while namespace selectors determine
which namespace profiles receive them. See the
[shared SSA policy reference](/docs/operating/concepts/managed-resources/) for the
complete contract and [migration guidance](/docs/replications/#deprecated-settings-and-migration)
for existing manifests using `spec.settings`.

#### Create

`creation: Owner` is the default. Capsule creates an absent object and applies future
changes to objects created by the applying controller. An unrelated existing target
is an ownership error. Capsule records whether each processed target was created or
adopted; `protect: true` blocks direct user updates and deletion while it is managed.

```yaml
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: namespace-profile
  namespace: solar-uat
spec:
  resources:
    - policy:
        creation: Owner
        force: false
        protect: true
        deletion: Remove
      rawItems:
        - apiVersion: v1
          kind: ConfigMap
          metadata:
            name: namespace-profile
          data:
            environment: production
```

SSA tracks ownership of individual fields. Multiple managers can contribute
non-conflicting fields to a shared object. A conflict is reported through
processed-item status; use [Force](#force) only when Capsule should take ownership
of those fields.

#### Adopt

Use `creation: Merge` to manage fields on an existing object, or create it if it is
absent. For example, this block adds a data entry to an existing ConfigMap:

```yaml
spec:
  resources:
    - policy:
        creation: Merge
        force: false
        protect: false
        deletion: Remove
      rawItems:
        - apiVersion: v1
          kind: ConfigMap
          metadata:
            name: shared-config
          data:
            capsule-profile: production
```

This example explicitly disables protection so other actors can continue managing
the ConfigMap. Adoption alone does not disable protection; the default remains
`protect: true`. Existing fields owned by other managers remain unless ownership
conflicts are intentionally resolved with `force`.

##### Pruning

Each block's `policy.deletion` controls cleanup when its targets leave scope,
including namespace deselection, block removal, or parent deletion:

| Policy | Created target | Adopted target |
| --- | --- | --- |
| `Remove` | Delete the object | Relinquish Capsule's applied fields and tracking |
| `Orphan` | Retain the object and content; remove lifecycle metadata | Retain the object and content; remove lifecycle metadata |

Cleanup uses the last successfully reconciled policy. A false apply condition does
not suppress cleanup, and changing `deletion` while the condition is false updates
the policy used for subsequent cleanup of already managed targets.

#### Conditional apply

Use `policy.condition` to control when rendered content is applied. It is evaluated
independently for every destination, with `object` set to the existing resource or
`null`, and `now` set to the evaluation timestamp:

```yaml
policy:
  condition: "object == null"
```

This creates a target only when absent. Rendering still happens before evaluation.
A false result reports `ConditionNotMet: apply skipped`, preserves content and its
last apply timestamp, and keeps already managed targets tracked. Policy changes
still take effect: protection metadata is reconciled and the current cleanup policy
is retained. Previously unowned targets remain untouched. See
[apply conditions](/docs/operating/concepts/managed-resources/#apply-conditions)
for validation, error handling, and field-retention semantics, and
[conditional age-key rotation](/docs/replications/global/#conditional-age-key-rotation)
for a complete stateful generator. A TenantResource can use the same resource block
within its tenant.

### DependsOn

A `TenantResource` can declare dependencies on other `TenantResource` objects in the same Namespace using `dependsOn`. The controller will not reconcile the resource until all declared dependencies are in `Ready` state.

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: gitops-secret
  namespace: wind-test
spec:
  resyncPeriod: 60s
  dependsOn:
    - name: custom-cm
  resources:
    - additionalMetadata:
        labels:
          projectcapsule.dev/tenant: "{{tenant.name}}"
      rawItems:
        - apiVersion: v1
          kind: Secret
          metadata:
            name: myregistrykey
            namespace: awesomeapps
          data:
            .dockerconfigjson: UmVhbGx5IHJlYWxseSByZWVlZWVlZWVlZWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGxsbGx5eXl5eXl5eXl5eXl5eXl5eXl5eSBsbGxsbGxsbGxsbGxsbG9vb29vb29vb29vb29vb29vb29vb29vb29vb25ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubmdnZ2dnZ2dnZ2dnZ2dnZ2dnZ2cgYXV0aCBrZXlzCg==
          type: kubernetes.io/dockerconfigjson
```

The status reflects whether a dependency is not yet ready:

```bash
kubectl get tenantresource -n wind-test

NAME                           ITEM COUNT   READY   STATUS                            AGE
custom-cm                      6            False   applying of 6 resources failed    12h
gitops-secret                  6            False   dependency custom-cm not ready    8h
```

If a dependency does not exist:

```bash
kubectl get tenantresource gitops-secret -n wind-test

NAME                           ITEM COUNT   READY   STATUS                            AGE
gitops-secret                  6            False   dependency custom-cm not found    8h
```

Dependencies are evaluated in the order they are declared in the `dependsOn` array.

---

### Force

Setting `spec.resources[].policy.force: true` instructs Capsule to [force-apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/#conflicts) changes on Server-Side Apply conflicts, claiming field ownership even if another manager already holds it.

**This option should generally be avoided.** Forcing ownership over a field managed by another operator will almost certainly cause a reconcile war. Only use it in scenarios where you intentionally want Capsule to win ownership disputes.

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-technical-accounts
  namespace: wind-test
spec:
  resources:
    - policy:
        creation: Owner
        force: true
        protect: true
        deletion: Remove
      generators:
        - template: |
            ---
            apiVersion: v1
            kind: ConfigMap
            metadata:
              name: shared-config
            data:
              common.conf: |
                {{ toYAML $.tenant.metadata | nindent 4 }}
```

---

### Context

The `context` field lets you load additional Kubernetes resources into the template rendering context. This is useful when you need to iterate over existing objects as part of your template logic. To inspect the full context available to a template, you can create a `ConfigMap` that dumps it:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: tenant-sops-providers
  namespace: wind-test
spec:
  resyncPeriod: 600s
  resources:
    - context:
        resources:
          - index: secrets
            apiVersion: v1
            kind: Secret
            namespace: "{{.namespace}}"
            selector:
              matchLabels:
                pullsecret.company.com: "true"
          - index: sa
            apiVersion: v1
            kind: ServiceAccount
            namespace: "{{.namespace}}"

      generators:
        - template: |
            ---
            apiVersion: v1
            kind: ConfigMap
            metadata:
              name: show-context
            data:
              context.yaml: |
                {{- toYAML $ | nindent 4 }}
```

A useful use case for this can be to add all imagePullSecrets to the `default` service account, so users don't have to add these manually to their deployment spec. In the example below, all the secrets with the label `imagePullSecret: "true"` are selected from the current Namespace. The names of these secrets are added to the `default` service account's `.imagePullSecrets` for all Namespaces of the Tenant:

```yaml
---
apiVersion: capsule.clastix.io/v1beta2
kind: TenantResource
metadata:
  name: imagepullsecrets-default-sa-green
  namespace: wind-test
spec:
  resyncPeriod: 600s
  resources:
    - policy:
        creation: Merge
        force: false
        protect: false
        deletion: Remove
      context:
        resources:
          - index: secrets
            apiVersion: v1
            kind: Secret
            namespace: "{{.namespace}}"
            selector:
              matchLabels:
                imagePullSecret: "true"
      generators:
        - template: |
            ---
            apiVersion: v1
            kind: ServiceAccount
            metadata:
              name: default
            {{- if $.secrets }}
            imagePullSecrets:
            {{- range $.secrets }}
              - name: {{ .metadata.name }}
            {{- end }}
            {{- end }}
```

#### Base Context

The following context is always available in generator templates. The `tenant` key is always present.

```yaml
tenant:
    apiVersion: capsule.clastix.io/v1beta2
    kind: Tenant
    metadata:
        creationTimestamp: "2026-02-06T09:54:30Z"
        generation: 1
        labels:
            kubernetes.io/metadata.name: wind
        name: wind
        resourceVersion: "4038"
        uid: 93992a2b-cba4-4d33-9d09-da8fc0bfe93c
    spec:
        additionalRoleBindings:
            - clusterRoleName: view
              subjects:
                - apiGroup: rbac.authorization.k8s.io
                  kind: Group
                  name: wind-users
        owners:
            - clusterRoles:
                - admin
                - capsule-namespace-deleter
              kind: User
              name: joe
        permissions:
            matchOwners:
                - matchLabels:
                    team: devops
                - matchLabels:
                    tenant: wind
    status:
        classes:
            priority:
                - system-cluster-critical
                - system-node-critical
            storage:
                - standard
        conditions:
            - lastTransitionTime: "2026-02-06T09:54:30Z"
              message: reconciled
              reason: Succeeded
              status: "True"
              type: Ready
            - lastTransitionTime: "2026-02-06T09:54:30Z"
              message: not cordoned
              reason: Active
              status: "False"
              type: Cordoned
        namespaces:
            - wind-prod
            - wind-test
        owners:
            - clusterRoles:
                - admin
                - capsule-namespace-deleter
              kind: Group
              name: oidc:org:devops
            - clusterRoles:
                - admin
                - capsule-namespace-deleter
              kind: User
              name: joe
        size: 2
        spaces:
            - conditions:
                - lastTransitionTime: "2026-02-06T09:54:30Z"
                  message: reconciled
                  reason: Succeeded
                  status: "True"
                  type: Ready
                - lastTransitionTime: "2026-02-06T09:54:30Z"
                  message: not cordoned
                  reason: Active
                  status: "False"
                  type: Cordoned
              metadata: {}
              name: wind-test
              uid: 24bb3c33-6e93-4191-8dc6-24b3df7cb1ed
            - conditions:
                - lastTransitionTime: "2026-02-06T09:54:30Z"
                  message: reconciled
                  reason: Succeeded
                  status: "True"
                  type: Ready
                - lastTransitionTime: "2026-02-06T09:54:30Z"
                  message: not cordoned
                  reason: Active
                  status: "False"
                  type: Cordoned
              metadata: {}
              name: wind-prod
              uid: b3f3201b-8527-47c4-928b-ad6ae610e707
        state: Active


namespace:
    apiVersion: v1
    kind: Namespace
    metadata:
        creationTimestamp: "2026-02-06T09:54:30Z"
        labels:
            capsule.clastix.io/tenant: wind
            kubernetes.io/metadata.name: wind-test
        name: wind-test
        ownerReferences:
            - apiVersion: capsule.clastix.io/v1beta2
              kind: Tenant
              name: wind
              uid: 93992a2b-cba4-4d33-9d09-da8fc0bfe93c
        resourceVersion: "3977"
        uid: 24bb3c33-6e93-4191-8dc6-24b3df7cb1ed
    spec:
        finalizers:
            - kubernetes
    status:
        phase: Active
```

### Monitoring

Currently mainly the conditions of `TenantResources` are exposed as metrics:

```
# HELP capsule_resource_condition The current condition status of a tenant resource.
# TYPE capsule_resource_condition gauge
capsule_resource_condition{condition="Cordoned",name="templated-forbidden-namespace",target_namespace="e2e-tenantresource-ssa-system"} 0
capsule_resource_condition{condition="Ready",name="templated-forbidden-namespace",target_namespace="e2e-tenantresource-ssa-system"} 1
```

---

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