Skip to content

Custom Resource Definitions

This documents the in-development version. Switch to v2.3 for the latest stable release.

kuik is configured with three cluster-scoped custom resources of the kuik.enix.io/v1alpha1 API group:

KindPurpose
ImageAlternativeRoutes images to alternative repositories holding the same images, as fallbacks or ahead of the original
ImageMirrorCopies images to a registry you own, and routes to the copy
ImageMonitorChecks that the images running in the cluster are still available upstream, and reports the ones that fail or drift

Every field below is validated by the API server (schema, enums, CEL rules), so an invalid resource is rejected at kubectl apply with a message naming the rule it broke. The full schema is available with kubectl explain imagealternatives.kuik.enix.io --recursive and its two siblings.

A resource applies to pods, never to images directly. podSelector and namespaceSelector are standard label selectors, and an empty or absent selector matches everything. Restricting a resource to one namespace is done with the namespace’s name label:

spec:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring

Excluding a workload is expressed on the resources that would otherwise apply to it; there is no operator-wide skip list.

Three gates sit ahead of every resource and cannot be configured: a container whose reference kuik itself produced is never rewritten again, a container with imagePullPolicy: Never is never rewritten, and a static pod (the kubelet’s mirror pod) is never rewritten.

Where a resource’s candidates sit relative to the original image when the webhook probes them:

ValueMeaningKinds
OnFailureDefault. The original image is tried first, the resource’s candidates are fallbacksImageAlternative, ImageMirror
AlwaysThe resource’s candidates are tried ahead of the original, to bypass a quota, latency or network costImageAlternative, ImageMirror
NoneNever routes. The mirror copies images without ever rewriting a podImageMirror only

When several resources cover one image, their candidates are merged in one list: Always mirrors, then Always alternatives, then the original, then OnFailure alternatives, then OnFailure mirrors, resources sorted by name within each band.

With webhook.availabilityCheck.demoteKnownFailures (on by default), the webhook then moves to the end of the list the candidates the background loops report failing:

  • the original, while any ImageMonitor lists it in status.unavailableImages, whatever that monitor’s selectors
  • a mirror candidate, while its own ImageMirror lists the pod’s image in status.failedImageCopies; another mirror of the same image keeps its place. A failed resync of a tag the mirror also lists in status.driftedImages does not count: the destination still serves the previous copy
  • an alternative candidate, while any ImageMonitor lists its reference in status.unavailableAlternatives with via naming the ImageAlternative that offered it, whatever its derivedFrom and that monitor’s selectors; the same reference offered by another ImageAlternative keeps its place

The moved candidates keep their relative order, after everything else, an original already sent last by an unavailable: true entry included. A moved candidate is still probed and served when everything above it fails, and a candidate reported available is never moved up. A moved Always candidate is served as a fallback, after the original failed, so it is recorded with policy: OnFailure, as an Always mirror is for a container with imagePullPolicy: Always. An entry counts as long as the status lists it, and the webhook only reads these statuses. Three limits:

  • an entry beyond the cap of its status list (truncated) is not seen, and its candidate keeps its place
  • a mirror candidate whose copy is still waiting, not yet failed, keeps its place: the first pods of a new image probe it and fall back as without the setting
  • via names only the first ImageAlternative offering an alternative, in candidate order: when several offer the same reference, only the candidate of that one moves

The webhook probes the candidates in order and places the first one that answers. It records its decision on the pod, in three annotations holding JSON objects keyed by container name. A container appears in at most one of them:

AnnotationThe containerValue
(none)kept its image: the original answered first
kuik.enix.io/rewriteswas rewritten{"by": "<kind>/<name>", "origin": ..., "rewrittenTo": ..., "policy": "Always" | "OnFailure"}
kuik.enix.io/conceded-rewriteswas rewritten, then another mutating webhook replaced the image, and kuik stood downthe entry that was in rewrites, unchanged
kuik.enix.io/no-alternativeskept its image: no candidate answeredthe resources that offered one, as ["<kind>/<name>", ...]

When the retained candidate’s auth injects a pull secret, the pod’s imagePullSecrets also gets kuik-inject-<kind>-<name>. The webhook exports two counters, kuik_routing_rewrites_total{kind, name, policy} and kuik_routing_alternatives_exhausted_total{kind, name}, listed with the reconciler’s events and series in Events, metrics and logs.

The credential kuik reads a registry with, wherever credentials appear on a resource: ImageAlternative entries, and an ImageMirror’s destination.manage and destination.pull. It is a union, and exactly one of secretRef or provider must be set:

auth:
secretRef: # a docker-registry Secret
name: quay-pull
injectPullSecret: true # default: true with secretRef
# --- or ---
auth:
provider: # an ambient cloud identity
name: aws # aws | gcp | azure
serviceAccountRef: # optional: request a token for this ServiceAccount instead of the controller's identity
name: mirror-pusher
injectPullSecret: false # default: false with provider
FieldDescription
secretRef.nameName of a kubernetes.io/dockerconfigjson Secret. It carries no namespace and always resolves in kuik’s install namespace, so referencing a Secret requires being able to write it there
provider.nameaws, gcp or azure
provider.serviceAccountRef.nameServiceAccount in kuik’s install namespace whose token is requested, so a resource carries its own cloud role
injectPullSecretWhether kuik copies a pull secret into the namespaces of the pods this credential serves, so the kubelet can pull the image. Defaults to true with secretRef and false with provider. A provider credential is not injected yet, whatever this field says. See Injected pull secrets

Without any auth, kuik injects nothing: the pod is expected to carry its own imagePullSecrets, or the operator’s fallbackAuth to cover the registry for kuik’s own reads.

repository, repositoryGroup and destination.path are fully qualified repository paths: a registry host (with an optional port), then path components, with no tag and no digest. quay.io/acme/foo and registry.local:5000/mirror/acme/foo are valid; acme/foo, quay.io/acme/foo:v1 and quay.io/acme/foo@sha256:… are rejected. The host is recognised as the container runtime does: it contains a dot, carries a port, or is localhost. A host alone (docker.io) is valid for repositoryGroup and destination.path, never for repository: a repository is one image with all its tags, docker.io/library/nginx.

Image references are normalized before any comparison, so nginx:1.27 is matched as docker.io/library/nginx:1.27.

Declares that several repositories hold the same images, so that a pod referencing one of them can be served from another.

apiVersion: kuik.enix.io/v1alpha1
kind: ImageAlternative
metadata:
name: acme-foo
spec:
podSelector: {}
namespaceSelector: {}
rewritePolicy: OnFailure # OnFailure | Always
alternatives:
- repository: quay.io/acme/foo
- repository: docker.io/acme-org/foo
- repository: 123456.dkr.ecr.eu-west-3.amazonaws.com/repo/acme/foo
auth:
provider:
name: aws
serviceAccountRef:
name: kuik-ecr-access
- repository: registry.local:5000/mirror/acme/foo
insecure: true
unavailable: true
auth:
secretRef:
name: local-registry
FieldRequiredDescription
spec.podSelectorPods this resource applies to. Empty matches every pod
spec.namespaceSelectorNamespaces this resource applies to. Empty matches every namespace
spec.rewritePolicyOnFailure (default) or Always, see rewritePolicy
spec.alternatives[]✅Ordered list of equivalent repositories, at least one entry. Every entry uses the same form, repository or repositoryGroup
spec.alternatives[].repositoryone ofMatches that exact repository, whatever the tag or digest
spec.alternatives[].repositoryGroupone ofMatches every repository located under that path, at any depth
spec.alternatives[].insecurekuik probes and pulls this registry over HTTP. The nodes’ container runtime must be configured for it separately
spec.alternatives[].unavailableDeclares the source gone: the entry still matches images, so the resource applies to them, but it is never offered, never read as a copy source and never monitored. An original image matching such an entry is tried last
spec.alternatives[].authCredential for this repository, see auth

An entry matches at path segment granularity, never as a string prefix: quay.io/acme/foo matches quay.io/acme/foo:v1 but never quay.io/acme/foo-bar:v1. The tag or digest is always carried over to the candidate, and for a repositoryGroup the remainder of the path is too.

Imagerepository: quay.io/acme/foorepositoryGroup: quay.io/acme/foo
quay.io/acme/foo:latestmatchesno: the group itself is not a repository
quay.io/acme/foo-bar:latestnono
quay.io/acme/foo/bar:latestno: deeper than the entrymatches, rewritten to <other>/bar:latest
quay.io/acme/foo/bar/oni:latestnomatches, rewritten to <other>/bar/oni:latest

Several ImageAlternative may match the same image; they add fallbacks to each other rather than shadowing each other. Digest-pinned images are routed like any other: the digest is carried over, and a candidate that does not hold it answers not found.

Rejected at admission:

  • an entry carrying both repository and repositoryGroup, or neither;
  • a repository or repositoryGroup carrying a tag or a digest, or not fully qualified with a registry host, and a repository naming a host alone;
  • a list mixing repository and repositoryGroup entries;
  • an empty alternatives list;
  • an auth carrying both secretRef and provider, or neither.
FieldDescription
status.pods.tracked / .rewrittenPods selected, and pods carrying at least one standing rewrite by this resource. Neither sums across resources
status.containers.*Containers of the selected pods, partitioned into untouched, rewritten, conceded (another webhook overwrote kuik’s rewrite), stale (edited after admission) and noAlternatives
status.activeFallbacks[]Origin images this resource currently stands in for, with rewrittenTo, pods and since. Rewrites recorded OnFailure only, which includes an Always candidate moved last as a known failure (demoteKnownFailures)
status.noAlternatives[]Images no candidate could serve
status.concededRewrites[] / status.staleRewrites[]Rewrites something replaced, with replacedBy
status.truncatedPer capped list, how many entries were left out, see bounded lists
status.conditionsReady, FallbackActive, AlternativesExhausted, ListCapacityPressure

The same counts, the anomalies as series and the events on the pods are in Events, metrics and logs.

Copies the images of the pods it selects to a destination registry, and routes to the copy according to rewritePolicy.

apiVersion: kuik.enix.io/v1alpha1
kind: ImageMirror
metadata:
name: prod-mirror
spec:
podSelector: {}
namespaceSelector: {}
excludeImages:
- ghcr.io/foo-bar/**
rewritePolicy: OnFailure # OnFailure | Always | None
destination:
path: registry.tld/mirror/
insecure: false
manage:
auth:
secretRef:
name: mirror-write-credentials
pull:
auth:
secretRef:
name: mirror-read-credentials
cleanup:
enabled: true
retention: 168h
driftPolicy: Ignore # Ignore | Warn | Sync
FieldRequiredDescription
spec.podSelectorPods this resource applies to. Empty matches every pod
spec.namespaceSelectorNamespaces this resource applies to. Empty matches every namespace
spec.excludeImages[]Globs of images to keep out of the mirror: no copy, no candidate, no tag at the destination. See excluding images
spec.rewritePolicyOnFailure (default), Always or None, see rewritePolicy
spec.destination.path✅Registry path the full original reference is appended to, hostname included: registry.tld/mirror/ turns docker.io/library/nginx:1.27 into registry.tld/mirror/docker.io/library/nginx:1.27_<clusterID>. The registry must accept deeply nested repository paths. Immutable: the tags written under a previous path could never be cleaned up
spec.destination.insecurekuik pushes to and reads this registry over HTTP
spec.destination.manage.authController credential at the destination: pushes, self-check reads, tag listings and deletions. Needs read as well as write. Its injectPullSecret is ignored
spec.destination.pull.authCredential the kubelet pulls the mirrored images with, injected in the namespaces that need it
spec.cleanup.enabledDefault true. Deletes the destination tags no pod references any more once their retention elapsed. With false nothing is deleted and retention has no effect
spec.cleanup.retentionDefault 168h. How long an unused tag is held before deletion, so a CronJob’s image survives between runs. A Go duration
spec.driftPolicyWhat to do when the upstream digest of a copied tag moves: Ignore (default, copy once), Warn (re-read the upstream periodically and report), Sync (re-read and copy again)

The tag of every copy carries the operator’s cluster ID as a _<clusterID> suffix, so that several clusters can share one destination: the second cluster to need an image uploads nothing, and each cluster only ever creates, verifies and deletes its own tags. The ImageMirror object is identical on every cluster.

Each excludeImages entry is a glob matched whole against the normalized reference. A : after the last / splits a tag part from a repository part; * matches inside one path segment, ** across segments.

PatternExcludes
ghcr.io/foo-bar/**everything under ghcr.io/foo-bar/, at any depth
ghcr.io/foo-bar/*one level under it: ghcr.io/foo-bar/app but not ghcr.io/foo-bar/team/app
docker.io/library/nginxthat repository, whatever the tag or digest
quay.io/acme/*-debugquay.io/acme/foo-debug, quay.io/acme/bar-debug
**:latestevery mutable latest, whatever the registry

The mirror’s own destination.path is excluded on top of this list, which is what keeps two mirrors from feeding each other.

The destination must conform to the OCI Distribution spec, accept arbitrarily nested repository paths, and refuse a manifest whose blobs it does not hold. With cleanup.enabled, it must also support tag deletion (DELETE /v2/<name>/manifests/<tag>) and run its own garbage collection of untagged manifests: kuik deletes tags only, never manifests or blobs. A registry that rejects tag deletion flips the Ready condition to False with reason RegistryDeleteUnsupported.

A deleted ImageMirror stops routing new pods and copying images at once, then stays Terminating until kuik has released it:

  1. While a live pod runs an image under destination.path, it waits: a node rescheduling that pod would find the copy gone. Roll those workloads onto their origin to release it.
  2. With cleanup.enabled, it then deletes every tag of this cluster in status.repositories, whatever their retention. It keeps waiting while the manage credential cannot be read, or while the destination refuses tag deletion: released, it would take with it the only inventory of the tags left behind.

It retries after 10 seconds, doubling up to 30 minutes, and sooner when a pod goes. A destination that never accepts tag deletion holds the mirror for good: set cleanup.enabled: false before deleting it to leave the tags in place.

The copy side first, then the routing side, which is field for field an ImageAlternative’s.

FieldDescription
status.images.copy.*References held at the destination: tracked (= running + standby + retained), available, unavailable, drifted, missingSource, orphanTags
status.driftedImages[]Tags whose upstream digest moved away from the copy, under Warn or Sync
status.failedImageCopies[]Origins whose copy has not succeeded, with reason (SourceNotFound, PushRejected, Unauthorized, QuotaExceeded, SourceUnreachable, DestinationUnreachable), since and lastAttempt
status.pendingDeletion[]Destination tags waiting out cleanup.retention, with origin when known and unusedSince. Not capped
status.selfCheckedEnd of the last full comparison against the destination
status.repositories[]Destination repositories this mirror wrote to, for the cleanup sweep. Not capped
status.checks.registries[]Health of the drift check rings, one per source host, under Warn or Sync. The destination never appears here
status.pods, status.containers, status.activeFallbacks[], status.noAlternatives[], status.concededRewrites[], status.staleRewrites[]The routing side, absent under rewritePolicy: None
status.conditionsReady, DestinationOutOfSync, FallbackActive, AlternativesExhausted, ListCapacityPressure

Checks the availability of the images of the pods it selects, and reports the ones that fail or drift. It never contributes a routing candidate.

apiVersion: kuik.enix.io/v1alpha1
kind: ImageMonitor
metadata:
name: cluster-images
spec:
podSelector: {}
namespaceSelector: {}
unusedImageRetention: 168h
driftDetection: true
monitorAlternatives: false
FieldRequiredDescription
spec.podSelectorPods this resource applies to. Empty matches every pod
spec.namespaceSelectorNamespaces this resource applies to. Empty matches every namespace
spec.unusedImageRetentionDefault 168h. Keeps monitoring an image for this long after no pod declares it any more, so a CronJob’s image stays checked between runs. A Go duration
spec.driftDetectionDefault true. Reports the tracked tags whose upstream digest differs from the one running in the cluster
spec.monitorAlternativesDefault false. Also monitors the alternatives kuik would offer for each tracked image, from ImageAlternative entries only. A mirror destination is verified by the mirror’s own self-check, and entries marked unavailable: true are never tracked

A monitor tracks the origin reference of every container: the reference its spec declares, or, where a standing kuik rewrite put a different one there, the origin that rewrite recorded. It therefore never sees a mirror’s reference in place of the origin it replaced.

The pods that count are the live ones (Pending or Running) the selectors select, static pods included. Init containers count, ephemeral containers never do. An image several pods declare counts once: running when a container runs that exact reference, standby when every container declaring it was routed elsewhere.

When no pod declares an image any more, it moves to status.retainedImages for unusedImageRetention, with the digest its most recently started container ran, and leaves it as soon as a pod declares it again. The monitor learns that an image went unused by watching its pods: an image whose last pod goes away while no reconciler runs is not retained.

Under monitorAlternatives, the monitor also tracks every candidate an ImageAlternative would offer for each origin it tracks, as the webhook would propose it for that container: only the ImageAlternatives whose selectors apply to the pod count, never an ImageMirror destination, never an entry marked unavailable (an origin such an entry names is still tracked as an origin), and never an entry naming the origin’s own repository. A container the webhook never rewrites, under imagePullPolicy: Never or in a static pod, offers no alternative. An alternative is running while a container carries it in place of its origin through a standing rewrite, standby otherwise. A retained origin keeps the alternatives its pods were offered when a pod last declared it, as long as their ImageAlternative still offers them (an entry marked unavailable or removed, or a deleted ImageAlternative, drops them); they count as retained without entering retainedImages, and leave once the origin’s retention elapses. After a restart, or once monitorAlternatives is turned off and on again, the monitor no longer knows what those pods were offered: the alternatives of a retained origin are then matched against every ImageAlternative, whatever its selectors, until its retention elapses. A reference that is both an origin and an alternative counts in both populations and is checked once. An ImageAlternative edit is followed without waiting for a pod to change, and turning monitorAlternatives off removes images.alternatives.

An alternative is checked on the rings like an origin, on a ring of its own host when no origin uses it, and is never compared to what runs: it never drifts. A check tries the auth of every entry offering it, in candidate order, then the pull secrets of the pods of its origins, then the matching fallbackAuth entry, then anonymously; an entry Secret that is missing or malformed is skipped for the next credential and leaves Ready as it is. An alternative several origins stand for is checked once, with the auth of each of their entries in turn. Each entry’s auth goes only over its own transport: the check reads over HTTPS with the auth of the secure entries, then the shared credentials (the pull secrets of the pods, fallbackAuth); when the registry refuses them and an entry is insecure, it reads again over plain HTTP with the auth of the insecure entries alone. The shared credentials cross plain HTTP only when no secure entry offers the alternative and it is no origin. images.alternatives.available and unavailable count the alternatives by the verdict of their last check, never among the origins: an alternative failing never enters unavailableImages nor emits AnonymousAccessDenied. A failing alternative enters status.unavailableAlternatives once per origin it stands for, with derivedFrom, reason, since, and via naming the first ImageAlternative offering it in candidate order (the Always band first, then by name). AlternativesUnavailable is True with reason ChecksFailed while any alternative fails. An alternative whose failure becomes Unauthorized emits AlternativeUnusable; any other failure emits nothing: alert on kuik_image_alternative_unavailable. A restart reads the entries of status.unavailableAlternatives back with their since, and does not announce an Unauthorized one again; for an alternative beyond the cap of the list, since is stamped anew and AlternativeUnusable may be emitted again. Until the rings have checked every alternative again, truncated, ListCapacityPressure and images.alternatives.unavailable stay at least as the status left them.

Checks are paced per registry host by the operator’s registries configuration: one image per window, so every tracked image comes back once per lap, the cycleDuration reported in status. See Registry pacing. Retained images are checked like the others. A restart resumes each ring after the cursor its status holds.

A check sends a manifest HEAD to the image’s registry with each credential in turn until one answers: the pull secrets of the pods declaring it, then the matching fallbackAuth entry, or one anonymous HEAD when none applies (see Registry credentials). A registry refusing an anonymous check with Unauthorized emits AnonymousAccessDenied on the monitor, once while the image stays unavailable: the image is likely private and no credential covers it. An image a fallbackAuth entry matches never emits it, even when that entry gives no credential: its missing or malformed Secret shows in Ready instead. When several monitors track an image, one check serves them all, and only the monitor whose ring sent the request emits the event or reports a broken fallbackAuth Secret in Ready. available and unavailable count the images by the verdict of their last check, so they fall short of tracked by the images the ring has not reached yet. A restart forgets the available verdicts: they come back as the rings turn, within one cycleDuration. An image whose check fails enters status.unavailableImages, its since stamped once and kept while the failure lasts, across restarts too, and leaves it as soon as a check answers. A restart reads back only the entries the capped list holds: for an image beyond the cap, since is stamped anew and AnonymousAccessDenied may be emitted again. Until the rings have checked every image again, truncated, ListCapacityPressure and images.origin.unavailable stay at least as the status left them. ImagesUnavailable is True with reason ChecksFailed while any origin fails. No event announces it: read it from the status, or alert on kuik_image_origin_unavailable.

Under driftDetection, a tag drifts when a pod runs it at a digest other than the one its last check read upstream: drift reads the digest of the availability check’s HEAD and sends no request of its own. The tag enters status.driftedImages with that upstreamDigest, a since stamped once, and runningDigests: every digest the containers carrying the tag itself run, each with the number of pods running it (a pod counts once per digest, init containers count). A container kuik routed to another reference, a container whose image is not pulled yet, a digest-pinned reference (name:tag@sha256:… included) and a retained image never drift. When the upstream moves again under a drifted tag, the entry takes the new upstreamDigest and keeps its since; when its check fails, it keeps the last digest read. The entry leaves as soon as every pod runs the upstream digest. ImagesDrifted is True with reason UpstreamDigestMoved while a tag drifts. A tag starting to drift emits ImageTagDrifted, and one whose pods start running several digests emits ImageClusterSkew and gets a kuik_image_cluster_skew_digests series; a drift that ends emits nothing. A restart reads the entries of status.driftedImages back with their since and upstreamDigest, recounts runningDigests from the pods at once, and announces none of them again. For a tag beyond the cap of the list, since is stamped anew, ImageTagDrifted and ImageClusterSkew may be emitted again, and its kuik_image_drifted and kuik_image_cluster_skew_digests series come back once its ring reaches it. Until a check has read the upstream digest of every running tag again, a failed check not counting, truncated, ListCapacityPressure and images.origin.drifted stay at least as the status left them, as far as the running tags whose drift is not known yet can account for. driftDetection: false reports no drift and removes what was reported. The digest a pod runs is the one its container runtime reports in imageID: containerd reports the index digest of a multi-arch image pulled by tag, which is what the check reads, while a runtime reporting a platform manifest or a config digest makes such a tag show as drifted (kubernetes#115199). An image the node had before any pull (preloaded, or loaded with kind load or ctr import) reports no registry digest in imageID, only its config digest: its drift is not checked, its retainedImages digest is empty, and the reconciler logs it once per image.

FieldDescription
status.images.origin.*Origin references: tracked (= running + standby + retained), available, unavailable, drifted
status.images.alternatives.*The same gauges for the monitored alternatives, under monitorAlternatives, without drifted: an alternative is checked, never compared to what runs
status.retainedImages[]References kept for unusedImageRetention, with unusedSince and digest. Not capped
status.unavailableImages[]Tracked origins whose last check failed, with reason (ManifestNotFound, Unauthorized, QuotaExceeded, Unreachable), since and pods
status.unavailableAlternatives[]Monitored alternatives whose last check failed, one entry per origin they stand for, with derivedFrom, via naming the ImageAlternative (ImageAlternative/<name>), reason and since
status.driftedImages[]Tracked tags whose running digest differs from the upstream one, with every runningDigests[] seen and its pod count
status.checks.registries[]One ring per registry host: images, cursor, cycleStarted, cycleDuration
status.truncatedPer capped list, how many entries were left out, see bounded lists. images.origin.unavailable and kuik_image_origin_unavailable still count every failing image, images.origin.drifted and kuik_image_drifted every drifted tag. images.alternatives.unavailable and kuik_image_alternative_unavailable cover every failing alternative. After a restart, the series of an image beyond the cap comes back once its ring reaches it
status.conditionsReady, ImagesUnavailable, AlternativesUnavailable, ImagesDrifted, ListCapacityPressure

Ready is the only condition that is True when things are well. Every other condition names an anomaly and stays True as long as it lasts, so “is anything wrong with this resource?” is one query: a condition whose status is True and whose type is not Ready. Ready goes False when the resource cannot work as declared (InvalidConfig, SecretNotFound, SecretMalformed, TokenRequestFailed, RegistryDeleteUnsupported), never because of one image.

Anomaly lists are bounded. unavailableImages, unavailableAlternatives, driftedImages, failedImageCopies, activeFallbacks, noAlternatives, concededRewrites and staleRewrites carry a fixed cap on their number of entries; they are samples, and the matching metrics carry every affected image. The oldest entries are kept. ListCapacityPressure goes True from 80% of the cap, and status.truncated records, per list, how many entries were left out once it is reached. The operational inventories (repositories, pendingDeletion, retainedImages, checks.registries) are never capped: truncating them would lose correctness rather than visibility.