Custom Resource Schemas
CRD structure and validation rules for site recovery resources
This page is the authoritative reference for every Custom Resource Definition (CRD) in the siterecovery.trilio.io/v1alpha1 API group. It documents each resource's purpose, its full spec and status schema, the validation rules enforced at admission, and the enum values accepted by each field. All DR operations in Trilio Site Recovery are declared through these custom resources — understanding their schemas is essential for configuring protection, triggering failovers, monitoring replication health, and interpreting operator status. Nine CRDs are registered on the TSR-Manager cluster and one (DRBDResource) is registered on each workload cluster.
Before working with these custom resources, ensure the following are in place:
- OpenShift 4.17 or later on all three clusters (TSR-Manager and both workload clusters). OpenShift 4.14–4.16 are within the supported floor but are not validated for Tech Preview.
- KubeVirt installed on both workload clusters (the
kubevirt.io/v1API group must be served withVirtualMachine,VirtualMachineInstance, andVirtualMachineInstanceMigrationkinds). - DRBD 9 kernel module (
drbd,drbd_transport_tcp,drbd_transport_lb-tcp) available on every workload node that will host a protected VM. - The Trilio Site Recovery operator installed on the TSR-Manager cluster via OLM (CatalogSource + OperatorGroup + Subscription for package
site-recovery, channeltech-preview). - A
ProtectionZoneCR that has reachedAvailablephase before anyProtectionGroup,ProtectionRequest,FailoverRequest,TestFailover, orHookcan be created or modified. TheClustersValidatedcondition on the ProtectionZone must beTrue; while it is not, the TSR-Manager admission webhooks reject those CR creates and spec changes. kubectland, for OpenShift-specific operations,oc.tsrctl(supplied by Trilio with the release; contact Trilio support to obtain it) for provisioning zone kubeconfig credentials before creating a ProtectionZone.- TCP ports 7000–8000 inclusive open between primary and DR worker nodes for DRBD replication (one port per replicated VM; port 8000 is mandatory — a firewall rule stopping at 7999 causes silent replication failure on one volume).
- RHEL CoreOS 9 nodes on workload clusters (RHEL CoreOS 10 is not validated in Tech Preview).
CRDs are installed automatically — you do not apply them manually. The installation sequence is:
1. Install the operator via OLM
Create the following objects on the TSR-Manager cluster in the trilio-site-recovery-system namespace:
apiVersion: operators.coreos.com/v1alpha1
kind: CatalogSource
metadata:
name: site-recovery-catalog
namespace: trilio-site-recovery-system
spec:
sourceType: grpc
image: quay.io/triliodata/site-recovery-catalog:<release-tag>
displayName: Trilio Site Recovery
publisher: Trilio Data, Inc.
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
name: site-recovery-og
namespace: trilio-site-recovery-system
spec: {}
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
name: site-recovery
namespace: trilio-site-recovery-system
spec:
channel: stable
name: site-recovery
source: site-recovery-catalog
sourceNamespace: trilio-site-recovery-system
Apply these with:
kubectl apply -f catalogsource.yaml \
-f operatorgroup.yaml \
-f subscription.yaml \
--context <tsr-manager-context>
OLM installs the ProtectionZone controller, the TSR web console (frontend and backend), and the ProtectionZone CRD. The remaining eight TSR-Manager CRDs (DRBDReplicationPolicy, FailoverRequest, Hook, ProtectionGroup, ProtectionRequest, ReplicationGroupStatus, RPOEvent, TestFailover) are installed by the ProtectionZone controller once a ProtectionZone is created. The DRBDResource CRD is installed on each workload cluster by the same controller.
2. Provision zone credentials
For each workload cluster, use tsrctl (obtained from Trilio support) to create a zone kubeconfig credential:
tsrctl zone-credential create \
--zone <zone-name> \
--kubeconfig <path-to-cluster1-admin-kubeconfig>,<path-to-cluster2-admin-kubeconfig>
This creates a per-zone ServiceAccount in the trilio-site-recovery-access namespace on each workload cluster and stores the resulting kubeconfig Secrets on the TSR-Manager cluster.
3. Create the zone namespace and ProtectionZone
Create the dr-<zone-name> namespace on the TSR-Manager cluster, then apply a ProtectionZone CR (see the ProtectionZone schema section below). Creating the ProtectionZone triggers the controller to validate both workload clusters, install the workload control-plane stacks and DRBD node agents, and register all remaining CRDs. All subsequent CRs (ProtectionGroup, ProtectionRequest, etc.) must be created in this same dr-<zone-name> namespace on the TSR-Manager cluster.
Verify that the ProtectionZone reaches Available phase:
kubectl get pz -n dr-<zone-name> --context <tsr-manager-context>
All configuration for Site Recovery is expressed through the custom resources described in this page. There are no separate configuration files for end users. The sections below describe the key configuration choices within each CRD.
API group and version
All Site Recovery CRDs share the API group and version:
siterecovery.trilio.io/v1alpha1
Cluster placement
| CRD | Where it lives | Where to query it |
|---|---|---|
ProtectionZone | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
ProtectionGroup | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
ProtectionRequest | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
DRBDReplicationPolicy | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
FailoverRequest | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
TestFailover | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
Hook | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
ReplicationGroupStatus | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
RPOEvent | TSR-Manager cluster, dr-<name> namespace | TSR-Manager |
DRBDResource | Each workload cluster, protected VM's namespace | Workload cluster only |
Running kubectl get drbdresource against the TSR-Manager cluster returns server doesn't have a resource type because the CRD is not registered there.
ClusterID values
Every field that references a cluster slot uses the ClusterID enum, which has exactly two values:
| Value | Meaning |
|---|---|
cluster1 | The first workload cluster slot |
cluster2 | The second workload cluster slot |
These are stable identifiers used across ProtectionZone, ProtectionGroup, ProtectionRequest, FailoverRequest, and DRBDReplicationPolicy. The human-readable cluster name lives in ProtectionZone.spec.clusters[].name.
Replication protocol
Only Protocol C (synchronous, RPO = 0) is supported in Tech Preview. The admission webhook rejects replicationProtocol: A with the message ReplicationProtocolA is not supported yet. Do not attempt to configure asynchronous replication.
Storage backend
Only storageBackendMode: DRBD is supported. The webhook rejects Linstor with Linstor backend is not supported yet; use DRBD.
Runtime log levels
Log levels for all Site Recovery components can be changed at runtime without pod restarts by editing the site-recovery-logging ConfigMap in the relevant namespace. Accepted values are error, info, debug, and trace. There is no warn level. The global key is level; per-component overrides are level.control-plane, level.drbd-node-agent, and level.protectionzone. The ProtectionZone spec.logging field propagates these settings automatically.
DRBDReplicationPolicy
The ProtectionZone controller creates one default DRBDReplicationPolicy named <zoneName>-default with isDefault: true in the zone namespace when a ProtectionZone is created. You do not create or modify this object. Only the default policy in the ProtectionRequest's namespace is consulted by the controller.
The following describes the most common usage patterns in the order you encounter them during normal operations.
Inspect all CRDs registered on the TSR-Manager cluster
kubectl get crd --context <tsr-manager-context> | grep siterecovery
Check ProtectionZone status
kubectl get pz -n dr-<zone-name> --context <tsr-manager-context>
The printer columns show Display Name, Backend, Clusters, Status (phase), and Age.
Check the ClustersValidated gate
Before you can create ProtectionGroups or ProtectionRequests, the ClustersValidated condition on the ProtectionZone must be True:
kubectl get pz <zone-name> -n dr-<zone-name> \
-o jsonpath='{.status.conditions[?(@.type=="ClustersValidated")]}' \
--context <tsr-manager-context>
List ProtectionGroups and their active cluster
kubectl get pg -n dr-<zone-name> --context <tsr-manager-context>
The Active column shows which cluster slot (cluster1 or cluster2) currently runs the VMs.
Check ProtectionRequest phase for a VM
kubectl get pr -n dr-<zone-name> --context <tsr-manager-context>
Phases in order: Pending → Validating → CreatingDRBD → Syncing → ReadyToActivate → Activating → Protected. A VM is fully protected only when the phase reaches Protected.
Trigger a planned failover
Create a FailoverRequest CR targeting the desired cluster:
kubectl apply -f failoverrequest.yaml -n dr-<zone-name> --context <tsr-manager-context>
Watch progress:
kubectl get fr <fr-name> -n dr-<zone-name> \
-o wide --context <tsr-manager-context>
The printer columns show Protection Group, Target, State, Phase, and Age. Drill into per-VM status:
kubectl get fr <fr-name> -n dr-<zone-name> \
-o jsonpath='{.status.vmStatuses}' \
--context <tsr-manager-context>
Run a test failover
kubectl apply -f testfailover.yaml -n dr-<zone-name> --context <tsr-manager-context>
Watch phases (Queued → PreparingEnvironment → CreatingSnapshots → CreatingVolumes → CreatingVMs → StartingVMs → Verifying → Succeeded):
kubectl get tfo <tfo-name> -n dr-<zone-name> --context <tsr-manager-context>
The test namespace (on the workload cluster) is reported in status.testNamespace. Do not construct or guess this value; read it from the status.
Inspect replication health for a ProtectionGroup
kubectl get rgs <pg-name>-replication -n dr-<zone-name> --context <tsr-manager-context>
The printer columns show Protection-Group, RPO-Status, Out-Of-Sync bytes, Synced VM count, Connection state, Last-Sync time, and Age.
Inspect DRBDResource status on a workload cluster
kubectl get dres -n <vm-namespace> --context <workload-cluster-context>
DRBDResource names follow the pattern drbd-<namespace>-<vm>-<hash8>. The printer columns show Side, Role, Node, State (connection), Phase, and Age.
Query RPO audit events for a ProtectionGroup
kubectl get rpoe -n dr-<zone-name> \
-l siterecovery.trilio.io/protection-group=<pg-name> \
--context <tsr-manager-context>
Use the label siterecovery.trilio.io/protection-group to filter RPOEvents and ReplicationGroupStatus objects — field selectors on spec.* or status.* are not supported on custom resources.
Query protected VMs on a workload cluster
kubectl get vm -A \
-l siterecovery.trilio.io/protected-vm=true \
--context <workload-cluster-context>
This label is stamped on the KubeVirt VirtualMachine only on the workload cluster.
Rotate a zone credential
tsrctl zone-credential rotate \
--zone <zone-name> \
--refresh-rbac
ProtectionZone — minimal example
This CR pairs two workload clusters in a zone using the DRBD storage backend. Both clusters run RHEL CoreOS 9 nodes.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
name: primary-dr-zone
namespace: dr-primary-dr-zone
spec:
displayName: Primary DR Zone
description: Two-cluster DRBD zone pairing the primary and DR sites.
storageBackendMode: DRBD
clusters:
- name: primary
clusterID: cluster1
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster1-kubeconfig
key: kubeconfig
- name: dr
clusterID: cluster2
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster2-kubeconfig
key: kubeconfig
ProtectionZone — with logging and node agent tuning
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionZone
metadata:
name: primary-dr-zone
namespace: dr-primary-dr-zone
spec:
displayName: Primary DR Zone
storageBackendMode: DRBD
clusters:
- name: primary
clusterID: cluster1
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster1-kubeconfig
key: kubeconfig
- name: dr
clusterID: cluster2
distributionName: RHEL-CoreOS-9
kubeconfigSecretRef:
name: cluster2-kubeconfig
key: kubeconfig
logging:
level: info
controlPlaneLevel: debug
nodeAgentLevel: info
nodeAgent:
startupDeadlineSeconds: 180
hooks:
jobTTLSeconds: 7200
failedJobRetentionSeconds: 604800
ProtectionGroup — two VMs with boot ordering
Both VMs fail over as one atomic unit. web-server boots first (lower bootOrder); app-server boots after.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionGroup
metadata:
name: web-tier
namespace: dr-primary-dr-zone
spec:
virtualMachines:
- name: web-server
namespace: production
bootOrder: 1
- name: app-server
namespace: production
bootOrder: 2
sourceCluster: cluster1
desiredState: running
sla:
rtoTargetSeconds: 300
rpoTargetSeconds: 0
ProtectionRequest — protect a single VM
The sourceCluster field is also the failover lever: patching it to the DR cluster triggers a role flip.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
name: protect-web-server
namespace: dr-primary-dr-zone
spec:
sourceCluster: cluster1
virtualMachine:
name: web-server
namespace: production
replicationConfig:
replicationProtocol: C
drbdMetadataConfig:
storageClassName: fast-ssd
size: 128Mi
ProtectionRequest — with DR-side placement override
When the DR cluster uses different node labels, use standbyPlacement to adjust the scheduling rules applied only to the DR copy. standbyPlacement is immutable once set.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: ProtectionRequest
metadata:
name: protect-web-server
namespace: dr-primary-dr-zone
spec:
sourceCluster: cluster1
virtualMachine:
name: web-server
namespace: production
replicationConfig:
replicationProtocol: C
standbyPlacement:
nodeSelector:
add:
topology.kubernetes.io/zone: dr-zone-a
remove:
- topology.kubernetes.io/zone
FailoverRequest — planned failover to cluster2
failoverType: planned stops VMs gracefully on the source cluster, waits for DRBD sync, then promotes the DR side.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
name: failover-web-tier-to-dr
namespace: dr-primary-dr-zone
spec:
protectionGroupRef:
name: web-tier
targetCluster: cluster2
failoverType: planned
batchBootTimeoutSeconds: 300
skipHooks: false
FailoverRequest — unplanned failover
failoverType: unplanned force-promotes the DR side without waiting for the source to stop cleanly. Use during a site outage when the primary cluster is unreachable.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: FailoverRequest
metadata:
name: emergency-failover-web-tier
namespace: dr-primary-dr-zone
spec:
protectionGroupRef:
name: web-tier
targetCluster: cluster2
failoverType: unplanned
TestFailover — DR drill with automatic cleanup
Spins up isolated VM copies from DRBD snapshots in a network-fenced namespace on the DR cluster. Resources are removed automatically after 2 hours.
apiVersion: siterecovery.trilio.io/v1alpha1
kind: TestFailover
metadata:
name: drill-web-tier-01
namespace: dr-primary-dr-zone
spec:
protectionGroupRef:
name: web-tier
cleanupPolicy: Automatic
retentionTime: 2h
schedulingCheck: Fail
verification:
reachability:
- type: tcp
vmName: web-server
port: 443
timeout: 30s
- type: http
vmName: web-server
port: 8080
endpoint: /healthz
expectedStatus: 200
timeout: 30s
After creating this CR, read the controller-generated test namespace from status:
kubectl get tfo drill-web-tier-01 -n dr-primary-dr-zone \
-o jsonpath='{.status.testNamespace}' \
--context <tsr-manager-context>
TestFailover — manual cleanup to preserve logs
apiVersion: siterecovery.trilio.io/v1alpha1
kind: TestFailover
metadata:
name: drill-web-tier-02
namespace: dr-primary-dr-zone
spec:
protectionGroupRef:
name: web-tier
cleanupPolicy: Manual
schedulingCheck: Warn
To trigger cleanup while keeping the TestFailover record:
kubectl patch tfo drill-web-tier-02 -n dr-primary-dr-zone \
--type merge \
-p '{"spec":{"cleanupRequested":true}}' \
--context <tsr-manager-context>
Hook — reusable lifecycle job definition
apiVersion: siterecovery.trilio.io/v1alpha1
kind: Hook
metadata:
name: notify-on-failover
namespace: dr-primary-dr-zone
spec:
description: Posts a Slack notification after failover completes.
jobTemplate:
image: quay.io/myorg/notifier:v1.2.3
command: ["/usr/bin/notify"]
args: ["--channel", "#dr-alerts"]
env:
- name: SLACK_WEBHOOK_URL
valueFrom:
secretKeyRef:
name: slack-credentials
key: webhook-url
timeoutSeconds: 60
retries: 2
retryDelaySeconds: 10
timeoutSeconds: 60
Bind this Hook to a ProtectionGroup by adding it to spec.hooks:
hooks:
- name: notify-post-failover
hookName: notify-on-failover
type: post-failover
runOn: target
onFailure: Continue
order: 0
Inspect ReplicationGroupStatus for a ProtectionGroup
The ReplicationGroupStatus is auto-created by the replication monitor. Its name is always <pg-name>-replication.
kubectl get rgs web-tier-replication -n dr-primary-dr-zone \
-o yaml --context <tsr-manager-context>
Inspect RPO audit events filtered by ProtectionGroup
kubectl get rpoe -n dr-primary-dr-zone \
-l siterecovery.trilio.io/protection-group=web-tier \
--context <tsr-manager-context>
Inspect DRBDResource status on a workload cluster
kubectl get dres -n production \
--context <cluster1-context>
To get per-node agent pod logs, first find the pod on the relevant node:
kubectl get pods -n trilio-site-recovery-system \
--field-selector spec.nodeName=<node-name> \
-l app.kubernetes.io/component=drbd-node-agent \
-o jsonpath='{.items[0].metadata.name}' \
--context <workload-cluster-context>
Then retrieve its logs:
kubectl logs <pod-name> -n trilio-site-recovery-system \
--context <workload-cluster-context>
ProtectionGroup or ProtectionRequest admission rejected
Symptom: kubectl apply returns a webhook denial when creating or updating a ProtectionGroup or ProtectionRequest.
Cause: The ClustersValidated condition on the ProtectionZone is not True. The TSR-Manager admission webhooks block all ProtectionGroup and ProtectionRequest creates and spec changes until both workload clusters have passed validation.
Fix: Check the zone status:
kubectl get pz <zone-name> -n dr-<zone-name> \
-o jsonpath='{.status.conditions}' \
--context <tsr-manager-context>
Resolve the specific condition reason (see the table below), then retry.
ProtectionZone validation failure reasons
| Condition reason | Meaning | Resolution |
|---|---|---|
ClusterAlreadyClaimed | The workload cluster's trilio-site-recovery-system namespace is already leased to another zone. | Each workload cluster can belong to only one zone. Delete the other zone or use a different cluster. |
InsufficientClusterPermissions | The zone credential authenticates but lacks required RBAC verbs. | Run tsrctl zone-credential rotate --refresh-rbac --zone <zone-name> to re-mint the credential with updated RBAC. |
ClusterIdentityChanged | The credential now reaches a cluster whose kube-system namespace UID differs from the one recorded at zone creation. | There is no override. Restore the original cluster or delete and re-create the zone. |
SCCPrerequisiteMissing | The privileged SCC or its ClusterRole does not exist on the workload cluster. | Ensure OpenShift is healthy and the privileged SCC is present before retrying zone creation. |
KubeVirtNotInstalled | The workload cluster does not serve kubevirt.io/v1. | Install OpenShift Virtualization (KubeVirt) on the workload cluster before creating the zone. |
SCCAdmissionDenied | The node-agent DaemonSet was rejected by OpenShift SCC admission after install. | Check the workload cluster's SCC configuration and ensure the agent's ServiceAccount is bound to the privileged SCC. |
ProtectionRequest stuck in CreatingDRBD or Syncing
Symptom: kubectl get pr shows the phase has not advanced after several minutes.
Cause: The DRBDResource on one or both workload clusters has not reached ReadyToActivate or Consistent.
Fix:
- Identify the DRBDResource name from the ProtectionRequest status:
kubectl get pr <pr-name> -n dr-<zone-name> \ -o jsonpath='{.status.drbdResourceRef}' \ --context <tsr-manager-context> - Check the DRBDResource on both workload clusters:
kubectl get dres <dres-name> -n <vm-namespace> \ -o yaml --context <cluster1-context> kubectl get dres <dres-name> -n <vm-namespace> \ -o yaml --context <cluster2-context> - Check the
status.phase,status.connectionState, andstatus.conditionsfields. AConditionPlacementReady=Falsecondition indicates metadata PVC provisioning has not completed. Verify that the metadata StorageClass binds immediately (WaitForFirstConsumer classes are rejected during validation). - Check the DRBD node agent logs on the relevant node (see the DRBDResource
spec.nodeNamefield to identify the node):kubectl get pods -n trilio-site-recovery-system \ --field-selector spec.nodeName=<node-name> \ -l app.kubernetes.io/component=drbd-node-agent \ -o jsonpath='{.items[0].metadata.name}' \ --context <workload-cluster-context> kubectl logs <pod-name> -n trilio-site-recovery-system \ --context <workload-cluster-context>
ProtectionRequest shows Ready=False, reason=RoleConflict
Symptom: The ProtectionRequest status.conditions contains a Ready=False condition with reason RoleConflict.
Cause: Both DRBDResource copies are reporting the DRBD Primary role simultaneously, which is an inconsistent state.
Fix: Do not attempt to manually patch DRBDResource CRs. Check for a stalled or incomplete failover by inspecting any recent FailoverRequest CRs in the zone namespace. If a planned failover did not complete, check the FailoverRequest status.phase and status.vmStatuses for the per-VM failure detail. Contact Trilio support if the conflict cannot be resolved by completing or deleting the stalled FailoverRequest.
DRBD replication appears disconnected despite network connectivity
Symptom: kubectl get dres shows State=Disconnected or State=StandAlone, but ping between the worker nodes succeeds.
Cause: TCP ports 7000–8000 may not be fully open. Each protected VM uses one port from this range; port 8000 is allocatable and is not optional. A firewall rule ending at 7999 silently blocks replication on the last allocated port.
Fix: Verify that ports 7000–8000 inclusive are open between primary and DR worker nodes. Check the specific port assigned to the VM from the DRBDResource:
kubectl get dres <dres-name> -n <vm-namespace> \
-o jsonpath='{.spec.port}' \
--context <workload-cluster-context>
TestFailover stuck or not progressing
Symptom: kubectl get tfo shows the phase has not advanced.
Cause: Common causes include: VolumeSnapshot not completing (check snapshot status on the workload cluster), test VMs not starting (check the test namespace on the workload cluster), or a hook failure with onFailure: Abort.
Fix:
- Read
status.testNamespacefrom the TestFailover:kubectl get tfo <tfo-name> -n dr-<zone-name> \ -o jsonpath='{.status.testNamespace}' \ --context <tsr-manager-context> - Inspect resources in that namespace on the non-active workload cluster.
- Check
status.hookStatusesfor any hook inFailedorTimedOutphase:kubectl get tfo <tfo-name> -n dr-<zone-name> \ -o jsonpath='{.status.hookStatuses}' \ --context <tsr-manager-context> - To bypass hooks during an incident, set
spec.skipHooks: trueon the next TestFailover CR (this field is immutable on an existing CR, so you must create a new one).
Node agent taint not removed after startup
Symptom: A workload node remains tainted with siterecovery.trilio.io/not-ready:NoSchedule after the startup deadline.
Cause: One or more DRBD resources on that node have not come up within the startupDeadlineSeconds window (default 120 seconds). The taint is removed at the deadline, but the affected DRBDResource carries NodeStartupGate=False.
Fix: This condition clears itself once the DRBD resource recovers — no manual action is required. Check the DRBDResource conditions on the workload cluster to identify what the agent is waiting for. Common causes are: DRBD kernel module not loaded (check agent init container logs), metadata PVC not bound, or peer connectivity not yet established after a node reboot. If the node agent's startup deadline is consistently too short for your hardware, increase it in the ProtectionZone:
spec:
nodeAgent:
startupDeadlineSeconds: 240
kubectl get drbdresource returns server doesn't have a resource type on TSR-Manager
Symptom: Running kubectl get dres or kubectl get drbdresource against the TSR-Manager cluster context returns an error.
Cause: The DRBDResource CRD is registered only on workload clusters, not on the TSR-Manager cluster.
Fix: Run DRBDResource queries against a workload cluster context:
kubectl get dres -n <vm-namespace> --context <workload-cluster-context>
Zone credential expiry warning
Symptom: The ProtectionZone has a CredentialExpiring reason on the CredentialsValid condition.
Cause: The zone kubeconfig credential for one or both workload clusters is approaching its expiry date (the zone warns 30 days before expiry). This is advisory and does not change the zone phase.
Fix: Rotate the credential before it expires:
tsrctl zone-credential rotate --zone <zone-name>