Skip to content

Maintenance

Kopia repositories need periodic maintenance to stay healthy. Maintenance compacts indexes, advances epochs, and, most importantly, reclaims storage by garbage-collecting content that deleted snapshots no longer reference. Without it, a repository keeps growing even as you expire old backups.

Kopiur makes maintenance a first-class, default-managed concern. You don’t have to remember to schedule it. Every Repository and ClusterRepository gets a Maintenance resource automatically, and the operator runs kopia maintenance on a schedule for every backend, filesystem and object stores such as S3, Azure, GCS and B2 alike.

kopia has two maintenance passes, and Kopiur schedules them independently:

Pass kopia command What it does Default schedule
Quick kopia maintenance run --no-full Cheap, frequent: index compaction, epoch advance. every 6h (0 */6 * * *), 30m jitter
Full kopia maintenance run --full Heavier: content garbage-collection + rewrite. This is what reclaims storage. daily at 03:00 (0 3 * * *), 1h jitter

A full run does everything a quick run does, so when both are due at once the operator runs full and advances both clocks.

Maintenance is on by default. For every Repository and ClusterRepository, the operator creates a Maintenance resource named after the repository, with the default schedule above. You can see it with:

Terminal window
$ kubectl get maintenance -A
NAMESPACE NAME REPOSITORY OWNER AGE
billing nas-primary nas-primary kopiur/billing/nas-primary 4h44m

There are three ways to control it, in increasing order of explicitness.

Watch the default-managed model work: a Maintenance appears with nothing but a Repository, and then force a run on demand. The bundle deploy/examples/tryit/maintenance.yaml is deliberately minimal. It has a namespace, a repo PVC, a KOPIA_PASSWORD Secret, and a filesystem Repository, and no Maintenance resource. The operator creates one for you.

The Repository below is the only CR in the bundle. It carries no spec.maintenance block and there is no standalone Maintenance; the managed one appears the moment this repository is Ready:

# A bare Repository. NOTE there is no `spec.maintenance` block and no standalone
# Maintenance CR: maintenance is default-on, so the operator projects an owned
# `Maintenance` named `primary` the moment this repository is Ready.
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: primary
namespace: kopiur-tryit
spec:
backend:
filesystem:
path: /repo
volume:
pvc:
name: kopia-repo
encryption:
passwordSecretRef:
name: repo-creds
key: KOPIA_PASSWORD
create:
enabled: true

Fill in the single REPLACE_ME (KOPIA_PASSWORD) and apply once:

Terminal window
$ kubectl apply -f deploy/examples/tryit/maintenance.yaml
$ kubectl -n kopiur-tryit wait --for=condition=Ready repository/primary --timeout=2m

1. The managed Maintenance appears on its own. You wrote none. The operator created primary, named after the repository, with the default quick-6h and full-daily schedule:

Terminal window
$ kubectl -n kopiur-tryit get maintenance
NAME REPOSITORY OWNER AGE
primary primary kopiur/kopiur-tryit/primary 20s

2. Request a full run NOW. Stamp the two on-demand annotations. Use --overwrite because the timestamp changes each time:

Terminal window
$ kubectl annotate maintenance primary -n kopiur-tryit --overwrite \
kopiur.home-operations.com/run-requested="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
kopiur.home-operations.com/run-mode=full

3. Prove the run completed (deep). The outcome lands in status.manualRun:

Terminal window
$ kubectl -n kopiur-tryit wait \
--for=jsonpath='{.status.manualRun.phase}'=Succeeded \
maintenance/primary --timeout=5m
$ kubectl -n kopiur-tryit get maintenance primary \
-o jsonpath='{.status.manualRun}'
{"requestedAt":"2026-06-17T14:05:00Z","mode":"full","phase":"Succeeded","completedAt":"2026-06-17T14:05:42Z"}

(Illustrative timestamps.)

4. Confirm the full clock advanced. A full run also stamps status.full.lastRunAt:

Terminal window
$ kubectl -n kopiur-tryit get maintenance primary \
-o jsonpath='{.status.full.lastRunAt}'
2026-06-17T14:05:42Z # illustrative

To tear down: kubectl delete namespace kopiur-tryit.

Set spec.maintenance on the Repository or ClusterRepository to override the schedule, or other settings, while keeping it operator-managed:

deploy/examples/maintenance-inline-on-repository.yaml
# Maintenance — tuned inline on the Repository (docs/src/content/docs/maintenance.mdx)
#
# The simplest way to control maintenance: set `spec.maintenance` on the
# Repository/ClusterRepository itself. Maintenance stays operator-managed (the
# controller projects and owns a Maintenance for you), you just override the
# schedule (or other knobs) instead of authoring a standalone Maintenance.
#
# This shows a filesystem (PVC-backed) repository, but the `spec.maintenance`
# block is identical for every backend. Omit the block entirely and you get the
# defaults (quick every 6h, full daily at 03:00); set `enabled: false` to opt out.
#
# Field shapes verified against crates/api: externally-tagged backend
# (`backend.filesystem`); `schedule.quick`/`schedule.full` are CronSpec
# { cron, jitter }.
---
apiVersion: v1
kind: Secret
metadata:
name: nas-primary-kopia
namespace: billing
type: Opaque
stringData:
# Filesystem backends need ONLY the repository encryption password.
KOPIA_PASSWORD: "choose-something-long-and-random"
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: nas-primary
namespace: billing
spec:
backend:
filesystem:
path: /repo # mount path INSIDE the mover pod
volume:
pvc:
name: nas-primary # the PVC mounted read-write at `path`
encryption:
passwordSecretRef:
name: nas-primary-kopia
key: KOPIA_PASSWORD
# Tune maintenance while keeping it operator-managed.
maintenance:
enabled: true
schedule:
quick: { cron: "0 */6 * * *", jitter: 30m } # cheap, frequent: index compaction
full: { cron: "0 3 * * *", jitter: 1h } # heavy: GC + rewrite — reclaims storage

spec.maintenance fields:

Field Purpose
enabled Default true. Set false to opt out (see Disabling).
schedule Override quick/full cron + jitter (and timezone).
mover Pod overrides for the maintenance Job (resources, scheduling, security context).
failurePolicy backoffLimit / activeDeadlineSeconds for the Job.
takeoverPolicy Ownership-lease policy (see Ownership).
namespace ClusterRepository only: which namespace the managed Maintenance lives in (defaults to the operator’s namespace).

For fine-grained control, such as a custom ownership identity or takeover policy, write a Maintenance directly (example 08). When one references a repository, the operator defers to it and never creates a duplicate, even if spec.maintenance is otherwise default-on.

apiVersion: kopiur.home-operations.com/v1alpha1
kind: Maintenance
metadata:
name: nas-primary-maintenance
namespace: billing
annotations:
# Request an immediate FULL run on next apply. Bump the timestamp to request
# another; remove these to stop requesting on-demand runs (the crons keep
# firing regardless).
kopiur.home-operations.com/run-requested: "2026-01-01T00:00:00Z"
kopiur.home-operations.com/run-mode: full
spec:
repository:
kind: Repository
name: nas-primary
schedule:
quick:
cron: "0 */6 * * *"
jitter: 30m
full:
cron: "0 3 * * 0"
jitter: 1h
timezone: UTC
ownership:
owner: "kopia-operator/nas-primary"
takeoverPolicy: PromptCondition
mover:
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
failurePolicy:
backoffLimit: 1
activeDeadlineSeconds: 14400 # cap a RUNNING maintenance Job (4h)
podStartupDeadlineSeconds: 300 # fail fast if the maintenance mover can't START in 5m (default 300)

Set spec.maintenance.enabled: false on the repository. The operator stops managing a Maintenance for it.

kopia tracks a single maintenance owner per repository. When several clusters, or several operators, share one repository, only one should run maintenance at a time. spec.ownership encodes who that is and what to do on conflict:

  • owner is a stable identity string for this Maintenance. The operator derives it for a managed Maintenance; see the lease format below.
  • ownerAliases lists previous lease strings still recognized as self. It is the migration path when the lease format changes, described below.
  • takeoverPolicy is a closed set of three values:
Policy Behavior when another owner holds the lease
Never (default, safest) Do nothing; surface that the lease is held elsewhere and wait.
PromptCondition Set a condition asking an operator to decide; don’t seize it.
Force Forcibly claim the lease and run.

The lease is read inside the maintenance Job, which is the only place with repository access for object stores. If the policy declines to take over, the run finishes successfully having done nothing, and records why on the resource’s conditions.

The lease string and kopia’s recorded owner

Section titled “The lease string and kopia’s recorded owner”

Every default-managed Maintenance derives its lease string (spec.ownership.owner) from the repository it covers. The format depends on whether the repository has identityDefaults.cluster set. A repository shared across clusters needs a distinct lease per cluster, or every cluster would derive the identical lease and fight over it:

Repository kind Cluster identity Lease string (spec.ownership.owner) kopia’s recorded owner (user@hostname)
Repository unset kopiur/<namespace>/<name> kopiur@kopiur-<namespace>-<name>
Repository east kopiur/east/<namespace>/<name> kopiur@kopiur.east.<namespace>.<name>
ClusterRepository unset kopiur/clusterrepository/<name> kopiur@kopiur-clusterrepository-<name>
ClusterRepository east kopiur/east/clusterrepository/<name> kopiur@kopiur.east.clusterrepository.<name>

ownerAliases — carrying ownership across a lease-format change

Section titled “ownerAliases — carrying ownership across a lease-format change”

spec.ownership.ownerAliases lists previous lease strings kopiur should still recognize as itself. This is the migration path for turning on identityDefaults.cluster for the first time on a repository that already has a managed Maintenance. The operator automatically records the pre-cluster lease as an alias on every managed Maintenance for a cluster-identified repository. A run then recognizes kopia’s already-recorded owner as its own and claims and re-stamps it to the new cluster-qualified format, instead of yielding to what would otherwise look like a foreign owner. You don’t set this by hand for a managed Maintenance. Only a standalone one needs it written out explicitly, if you hand-roll the same migration.

Sharing one repository’s maintenance across clusters: pick ONE owner

Section titled “Sharing one repository’s maintenance across clusters: pick ONE owner”

kopia has no cross-host lock beyond this lease. So when several clusters’ Repository or ClusterRepository objects all point at the same physical repository, each cluster’s independently-managed Maintenance tries to claim the same lease. The default, takeoverPolicy: Never, makes that safe, because every cluster but the current holder yields. It still isn’t the recommended steady state once you know which cluster should own it:

  • Pick the one cluster that runs maintenance for the shared repository. kopia doesn’t care which.
  • On every other cluster, set spec.maintenance.enabled: false on that repository. The operator then stops creating and reconciling a Maintenance there at all, instead of running one that exists only to yield forever.
  • Remove takeoverPolicy: Force from every cluster except the owner. Force unconditionally seizes the lease on its next run. Leave it set on more than one cluster and they fight over it every reconcile, each re-seizing it from the other. Set Force only on the one cluster you intend to own it, and only long enough to claim the lease once, then revert it to Never.

Yielding is the safety net, not the recommended posture. A non-owner cluster left with maintenance enabled just yields loudly, showing Ready=False with reason MaintenanceYielding, rather than fighting for the lease or touching data. But that loud yielding, plus a mover Job that runs for nothing every cron slot, is exactly why the explicit enabled: false posture above is preferred once you know the owner. See Share one repository across clusters for the full walkthrough, with this step placed in the correct order relative to the identity flip.

kopia stamps a maintenance owner when a repository is created. kopiur stamps its own stable, lease-derived owner, per the table above, so that every maintenance Job, each running from a fresh throwaway pod, recognizes itself as the owner and runs. On every bootstrap, meaning the initial connect and each catalog refresh, if the recorded owner doesn’t match, kopiur may re-stamp it. kopia maintenance set --owner is not owner-gated. How readily it re-stamps depends on whether the repository has a cluster identity:

  • No identityDefaults.cluster, a single-cluster repository. Self-healing is unchanged from before multi-cluster support: any stale owner is re-stamped unconditionally. That is safe, because at most one cluster’s operator ever bootstraps this repository. A stale owner can only be kopia’s own short-lived create-time identity, such as nonroot@nas-bootstrap-5trlr, or an older-format stamp from this same operator. It is never another cluster’s.
  • identityDefaults.cluster set, a shared repository. Self-heal re-stamps only an owner that is empty, already the desired owner, or matches a recognized ownerAliases entry. That is, only this cluster’s own current or legacy formats. A foreign cluster’s owner is always honored and left completely alone. Under the unconditional rule, every cluster sharing the repository would see the other’s owner as “stale” and re-claim it on its own next bootstrap, bouncing the lease back and forth forever.
  • The tradeoff: an ancient owner this operator has never recognized before is left alone rather than clobbered automatically, even though it may genuinely be stale. That covers a workstation kopia CLI, or any owner string that isn’t this cluster’s current lease or a registered alias. Move it with a one-time spec.ownership.takeoverPolicy: Force, or with kopia maintenance set --owner by hand. Once claimed, this operator’s own lease-derived owner is recognized on every subsequent run, so no further manual steps are needed. Revert Force to Never right after; see the “pick ONE owner” section above for why leaving it set is unsafe on a shared repository.

kopia stores its content index as a set of index blobs. Each backup adds one, and maintenance compacts them back down. So in a healthy repository the count rises during the day and falls after the next full-maintenance run. If maintenance stops keeping up, most often because of a stale owner as described above, but also because of a disabled or failing Maintenance, the count climbs without bound. Once it gets high enough, kopia warns “Found too many index blobs (N)” and backup and restore performance degrades.

kopiur observes the count at bootstrap and after each successful maintenance run — the newer of the two observations wins — and surfaces it three ways, without blocking the repository. The repository stays Ready; this is a degradation warning, not an outage:

  • a print column: kubectl get repository and kubectl get clusterrepository show an IndexBlobs column in wide output;
  • status.storageStats.indexBlobCount;
  • when the count crosses the threshold, an IndexBlobHealth=False condition with reason TooManyIndexBlobs, and a Kubernetes Warning event with the remediation in its message:
Terminal window
$ kubectl describe clusterrepository nas-shared | grep -A2 TooManyIndexBlobs
$ kubectl get events --field-selector reason=TooManyIndexBlobs -A

spec.health.indexBlobWarnThreshold sets the count above which the warning fires. It’s optional on both Repository and ClusterRepository:

Value Meaning
absent Use the built-in default of 1000 (well above a healthy repo).
a positive number Warn when the count exceeds it (lower = earlier warning).
0 Disable the warning entirely.
spec:
health:
indexBlobWarnThreshold: 500 # warn sooner than the default 1000

When maintenance is running and the count still won’t fall

Section titled “When maintenance is running and the count still won’t fall”

Raising the threshold hides the warning. It doesn’t compact anything, it has no effect on kopia’s own hard-coded “Found too many index blobs” message, and it doesn’t change how long your movers take to connect. If maintenance is demonstrably running and the count is still stuck in the thousands, the cause is usually kopia’s epoch-advance gate, and no maintenance schedule can fix it.

kopia cannot compact an index blob until its epoch closes. An epoch may only close once it is older than MinEpochDuration, which is 24h by default, no matter how many blobs accumulate inside it. Compaction then trails two epochs behind. So a fleet producing, say, around 60 index blobs an hour is forced to about 1700 blobs before an epoch may close, and permanently carries 1700 to 3400 uncompacted blobs. Compaction pace was never the bottleneck. The 24h floor is.

spec.parameters.epoch exposes that gate on both Repository and ClusterRepository:

spec:
parameters:
epoch:
minDuration: 6h # kopia's default is 24h

Every field is optional and kopiur has no defaults of its own. Absent means “leave kopia’s current value alone”, so a repository that declares nothing here is completely unaffected. Applying a change rewrites the repository’s format blob, which invalidates other kopia clients’ cached copy of it. They re-read within about 15 minutes. Because of that, kopiur calls set-parameters only when the declared values actually differ from what the repository reports.

Check what landed under status.parameters.epoch. It reports what the repository says, not what you asked for, so a value that failed to apply shows up as a mismatch rather than as silence:

Terminal window
$ kubectl get repository primary -o jsonpath='{.status.parameters.epoch}' | jq
{
"enabled": true,
"minDuration": "6h",
"refreshFrequency": "20m",
"cleanupSafetyMargin": "4h",
"advanceOnCount": 20,
"advanceOnSizeMiB": 10,
"checkpointFrequency": 7,
"deleteParallelism": 4
}

The full field set, plus the sharp edges:

deploy/examples/33-repository-epoch-parameters.yaml
# Example 33 — tune kopia's epoch parameters (spec.parameters.epoch)
#
# THE SYMPTOM: `IndexBlobHealth=False` / `TooManyIndexBlobs`, thousands of index
# blobs that never go down however often maintenance runs, and mover connects that
# take tens of seconds because every one of them reads those blobs.
#
# THE CAUSE, if maintenance is demonstrably running: kopia cannot compact an index
# blob until its EPOCH closes. An epoch may only close once it is older than
# MinEpochDuration — 24h by default — no matter how many blobs accumulate inside it.
# Compaction then trails two epochs behind. So a fleet producing ~60 index blobs an
# hour is held at ~1700 blobs per epoch and carries 1700–3400 uncompacted blobs
# permanently. No maintenance schedule can fix that: compaction pace is not the
# bottleneck, the 24h advance gate is.
#
# THE FIX: lower minDuration so epochs close (and blobs become compactable) sooner.
# 6h is a reasonable starting point for a busy repository.
#
# WHY THIS IS A CR FIELD: `kopia repository set-parameters --epoch-min-duration=6h`
# run by hand works — the value persists in the format blob and movers pick it up on
# connect — but it is invisible to GitOps, survives in no manifest, and a repository
# re-bootstrap silently reverts it. Declared here, kopiur re-applies it whenever it
# drifts.
#
# ABSENT MEANS "DON'T TOUCH". Every field below is optional and kopiur has no
# defaults of its own: omit one and kopia's current value is left exactly as it is.
# The flip side is that REMOVING a value you previously set does not restore kopia's
# default — it leaves the repository at whatever you last applied. Set it back
# explicitly if you want the default again.
#
# NOTE: applying these rewrites the repository's format blob, which invalidates every
# other kopia client's cached copy (they re-read within ~15 minutes on their own).
# kopiur therefore only calls set-parameters when the declared values actually differ
# from what the repository reports — a converged spec costs nothing.
#
# NOT AVAILABLE on a `mode: ReadOnly` repository: set-parameters is a repository-wide
# write and kopia refuses it on a read-only connection, so declaring it there is
# rejected at admission. In a multi-cluster layout, declare the parameters on the
# cluster that owns the repository — they are a property of the repository, not of
# each consumer.
#
# See docs/src/content/docs/repository-health.mdx
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Repository
metadata:
name: primary
namespace: backups
spec:
backend:
s3:
bucket: kopia-backups
endpoint: s3.example.com
region: us-east-1
auth:
secretRef:
name: s3-creds
encryption:
passwordSecretRef:
name: kopia-repo
key: KOPIA_PASSWORD
parameters:
epoch:
# The gate. kopia's default is 24h; an epoch cannot close before this,
# regardless of how many index blobs it holds.
minDuration: 6h
# The rest are shown for reference — omit them unless you have a reason.
# Each comment is kopia's default, which is what you get by leaving it out.
#
# refreshFrequency: 20m # how often clients re-read epoch state
# advanceOnCount: 20 # index blobs that trigger an advance (once past minDuration)
# advanceOnSizeMiB: 10 # index size that triggers an advance (MEBIbytes: 10 = 10485760 bytes,
# # despite kopia's own log rendering it as "10 MB")
# checkpointFrequency: 7 # epochs between full index checkpoints
# deleteParallelism: 4 # parallelism for epoch cleanup deletions
# Observe the result under `status.parameters.epoch`:
#
# kubectl get repository primary -n backups -o jsonpath='{.status.parameters.epoch}'
#
# It reports what the REPOSITORY says, not what you declared, so a value that
# failed to apply shows up as a mismatch rather than as silence.
#
# `cleanupSafetyMargin` appears there too but is deliberately not settable: it is
# the grace window that stops kopia deleting index blobs a concurrent writer still
# needs, and there is no safe generic advice for lowering it.

Three things worth knowing:

  • Removing a value does not restore kopia’s default. Under “absent means don’t touch”, deleting minDuration from your manifest leaves the repository at 6h forever. Set it back explicitly if you want 24h again.
  • mode: ReadOnly repositories cannot declare parameters. set-parameters is a repository-wide write and kopia refuses it on a read-only connection, so the combination is rejected at admission. In a multi-cluster layout, declare parameters on the cluster that owns the repository. They describe the repository, not each consumer. Two clusters declaring different values for one repository will fight over it.
  • cleanupSafetyMargin is reported but not settable. It is the grace window that stops kopia deleting index blobs a concurrent writer still needs, and there is no safe generic advice for lowering it.

spec.parameters has a second key, blobRetention, which turns on S3, Azure and GCS object lock so a compromised credential cannot delete your backups. It rides the same set-parameters call as epoch, one invocation applied only on drift, and it changes what maintenance can reclaim. Deletes are still issued, but the storage layer refuses them until locks expire, so the repository grows for at least the retention period. See Object lock.

Maintenance normally fires on its quick and full crons. You can request an out-of-band run at any time by stamping two annotations on the Maintenance. The operator routes it through the same mover, ownership-lease and one-at-a-time path as the scheduled slots, so a manual run can never break the one-job-per-repository rule.

Declaratively, which is GitOps-friendly, set them in Git on the managed Maintenance, as in example 08:

metadata:
annotations:
# A NEW value requests a new run; re-applying the same value is a no-op.
kopiur.home-operations.com/run-requested: "2026-01-01T00:00:00Z"
kopiur.home-operations.com/run-mode: full # quick (default when absent) | full
  • run-requested is an RFC3339 timestamp. A new timestamp requests a new run; re-applying the same value does nothing once that request was handled.
  • run-mode is quick, the default when absent, or full.

The outcome lands in status.manualRun, which carries requestedAt, mode, phase (Running, Succeeded or Failed), and completedAt. The kubectl plugin wraps this as kubectl kopiur maintenance run [NAME | --repository NAME] [--full] [--wait].

Terminal window
$ kubectl get maintenance nas-primary -n billing -o yaml

Key status fields:

Field Meaning
ownership.owner / ownership.claimedAt Current lease holder and when it was claimed.
quick.lastRunAt / full.lastRunAt Timestamp of the most recent run of each pass.
quick.lastHandledAt / full.lastHandledAt The most recent cron slot whose Job finished, including a yield (which doesn’t move lastRunAt), so a handled slot never re-fires.
quick.lastContentReclaimedBytes / full.lastContentReclaimedBytes Storage reclaimed. The only place this is surfaced.
conditions[type=LeaseOwned] True when this resource holds the lease and is running; False (with a reason) when waiting on the repository, a held lease, or a failure.

The running mover Jobs are labeled, so you can watch them directly:

Terminal window
$ kubectl get jobs -n billing -l app.kubernetes.io/component=maintenance
  • Runs at the scheduled time. Spawning is gated on the same cron and jitter logic as SnapshotSchedule, seeded deterministically per resource, so two replicas agree and the run lands in its window. It is not “every reconcile”.
  • Waits for the repository, but only when maintenance can’t help. Maintenance runs once the target repository has ever bootstrapped, and keeps running even while the repository is Degraded. It only defers when the backend is confirmed unreachable or the repository vanished (BackendReachable=False with reason BackendUnreachable or RepositoryVanished), when the repository has never bootstrapped, or when it is in a terminal Failed state. A repository that is Degraded only because its connects keep exceeding the bootstrap deadline (ProbeDeadlineExceeded) still gets maintenance: index compaction is usually the cure for a slow connect, so withholding it would deadlock the repository (#413). While deferred, the resource shows LeaseOwned=False, reason=WaitingForRepository.
  • One run at a time. The operator never starts a second maintenance Job for a repository while one is in flight.
  • Catches up after downtime, once. If the operator is down across several scheduled slots, it runs a single catch-up pass on recovery, not a storm of missed runs.
  • Self-cleaning Jobs. Finished maintenance Jobs are removed automatically via ttlSecondsAfterFinished, and a failed run is retried with backoff.
  • A handled slot never re-fires. Each scheduled slot runs once. Its outcome, either a real run or a deliberate yield to a foreign lease holder, is recorded durably in status.<quick|full>.lastHandledAt, so the Job self-cleanup above cannot make the same slot run again. Only a failed slot is retried.
  • Yielding is loud, not silent. When every run yields, meaning a foreign owner holds the lease and takeoverPolicy is Never, kopia garbage collection and compaction are not happening. The resource reports Ready=False with reason MaintenanceYielding and the takeoverPolicy: Force remediation in the message, instead of a misleading Ready=True.