Image routing
When a Pod is created, kube-image-keeper’s mutating webhook evaluates every container image against the declared (Cluster)ImageSetMirror and (Cluster)ReplicatedImageSet resources. It builds an ordered list of alternative images and rewrites the container to use the first available one.
Default ordering
Section titled “Default ordering”Without any explicit priority, alternatives are ordered as follows:
- Original image
ClusterImageSetMirrormirrorsImageSetMirrormirrorsClusterReplicatedImageSetupstreamsReplicatedImageSetupstreams
Within each resource, mirrors or upstreams are listed in their YAML declaration order. The webhook performs a HEAD on each alternative image manifest (in order) and uses the first one that is available.
Priority system
Section titled “Priority system”A two-level priority system allows fine-grained control over this ordering. It works like the Linux nice value: lower values mean higher priority.
Level 1: CR priority (spec.priority)
Section titled “Level 1: CR priority (spec.priority)”Every (Cluster)ImageSetMirror and (Cluster)ReplicatedImageSet accepts a signed integer spec.priority field (default 0).
| Value | Behavior |
|---|---|
| Negative | Alternatives from this CR are placed before the original image, effectively overriding it when available. |
0 (default) | The original image is tried first; alternatives from this CR serve as fallback. |
| Positive | Alternatives from this CR are tried after the original, but with lower priority than CRs with priority: 0. |
When multiple CRs share the same priority, the default type ordering applies (ClusterImageSetMirror > ImageSetMirror > ClusterReplicatedImageSet > ReplicatedImageSet).
Example: always prefer a private mirror over the original image:
apiVersion: kuik.enix.io/v1alpha1kind: ClusterImageSetMirrormetadata: name: private-mirrorspec: priority: -1 imageFilter: include: - .* mirrors: - registry: registry.example.com path: /mirror credentialSecret: name: registry-secret namespace: kuik-systemWith priority: -1, the mirrored image is checked before the original. If it is available, the pod is rewritten to use it.
Level 2: intra-CR priority (mirrors[].priority / upstreams[].priority)
Section titled “Level 2: intra-CR priority (mirrors[].priority / upstreams[].priority)”Each mirror or upstream entry accepts an unsigned integer priority field (default 0) that controls ordering within the same CR.
| Value | Behavior |
|---|---|
0 (default) | Default position; YAML declaration order is preserved among items at priority 0. |
| Positive | Sorted ascending: lower value = higher priority. |
Example: prefer Quay over ECR over Docker Hub:
apiVersion: kuik.enix.io/v1alpha1kind: ClusterReplicatedImageSetmetadata: name: nginx-unprivilegedspec: upstreams: - registry: docker.io priority: 30 imageFilter: include: - /nginxinc/nginx-unprivileged:.+ path: /nginxinc/nginx-unprivileged - registry: quay.io priority: 10 imageFilter: include: - /nginx/nginx-unprivileged:.+ path: /nginx/nginx-unprivileged - registry: public.ecr.aws priority: 20 imageFilter: include: - /nginx/nginx-unprivileged:.+ path: /nginx/nginx-unprivilegedFor a pod requesting docker.io/nginxinc/nginx-unprivileged:1.29, this produces the following alternative order:
docker.io/nginxinc/nginx-unprivileged:1.29(original image, at CR priority0)quay.io/nginx/nginx-unprivileged:1.29(intra-priority10)public.ecr.aws/nginx/nginx-unprivileged:1.29(intra-priority20)docker.io/nginxinc/nginx-unprivileged:1.29(intra-priority30, deduplicated with original so it will not be checked)
Combining both levels
Section titled “Combining both levels”Both levels compose naturally. The full sort key is:
- CR priority (
spec.priority) — ascending - Type order — ClusterImageSetMirror > ImageSetMirror > ClusterReplicatedImageSet > ReplicatedImageSet
- Intra-CR priority (
mirrors[].priority/upstreams[].priority) — ascending - Declaration order — YAML position within the CR
The original image is inserted at CR priority 0, before any CR alternative at the same priority.
Example: combining a namespace-scoped mirror with a cluster-wide mirror:
# Cluster-wide mirror, slight preference over originalapiVersion: kuik.enix.io/v1alpha1kind: ClusterImageSetMirrormetadata: name: global-mirrorspec: priority: -1 imageFilter: include: - .* mirrors: - registry: harbor.example.com path: /global-mirror credentialSecret: name: harbor-secret namespace: kuik-system---# Namespace-scoped mirror, strong preference over originalapiVersion: kuik.enix.io/v1alpha1kind: ImageSetMirrormetadata: name: team-mirror namespace: my-appspec: priority: -10 imageFilter: include: - docker-registry.example.com/my-app/.+ mirrors: - registry: fast-registry.internal priority: 1 path: /my-app-cache credentialSecret: name: fast-registry-secret - registry: harbor.example.com priority: 5 path: /my-app-mirror credentialSecret: name: harbor-secretFor a pod in namespace my-app requesting docker-registry.example.com/my-app/api:v2, the resulting order is:
fast-registry.internal/my-app-cache/my-app/api:v2(CR priority-10, intra-priority1)harbor.example.com/my-app-mirror/my-app/api:v2(CR priority-10, intra-priority5)harbor.example.com/global-mirror/my-app/api:v2(CR priority-1)docker-registry.example.com/my-app/api:v2(original image, priority0)
Discarding an upstream without removing it (discardAlternative)
Section titled “Discarding an upstream without removing it (discardAlternative)”Setting discardAlternative: true on a (Cluster)ReplicatedImageSet upstream keeps the entry in the configuration but excludes it from the list of alternatives the webhook tries. The upstream still participates in image matching (its imageFilter can trigger the CR), so other upstreams in the same CR continue to route correctly.
This is useful when a source registry has been migrated or deleted: without this flag the webhook tries the dead registry on every pod admission and waits for the full check timeout before moving on. With discardAlternative: true the dead upstream is skipped immediately.
apiVersion: kuik.enix.io/v1alpha1kind: ClusterReplicatedImageSetmetadata: name: bitnamispec: upstreams: - registry: docker.io path: /bitnami imageFilter: include: - /bitnami/.* discardAlternative: true # old location, no longer reachable - registry: registry.bitnami.com path: /bitnami imageFilter: include: - /bitnami/.*In this example, pods referencing docker.io/bitnami/nginx:latest will be routed directly to registry.bitnami.com/bitnami/nginx:latest without attempting docker.io first.
Interaction with imagePullPolicy
Section titled “Interaction with imagePullPolicy”By default, containers with imagePullPolicy: Always always pull the original image first; spec.priority on (Cluster)ImageSetMirror / (Cluster)ReplicatedImageSet is not honored for those containers. This preserves the semantic that Always should reach the upstream registry even when a higher-priority mirror is declared.
To honor priorities for Always containers as well (for example, in clusters where Always is enforced cluster-wide and you want to route through a private mirror to avoid upstream rate limits), set routing.honorPrioritiesOnAlwaysImagePullPolicy: true in the operator configuration (or in the Helm values). With this flag set, a negative spec.priority will route through the mirror before the original image regardless of pull policy.
Containers with imagePullPolicy: Never are skipped entirely by default; this can be flipped with routing.rewriteOnNeverImagePullPolicy: true.
See the full operator configuration reference for the list of all supported fields, their defaults, and the precedence rules.