Skip to content

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.

Without any explicit priority, alternatives are ordered as follows:

  1. Original image
  2. ClusterImageSetMirror mirrors
  3. ImageSetMirror mirrors
  4. ClusterReplicatedImageSet upstreams
  5. ReplicatedImageSet upstreams

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.

A two-level priority system allows fine-grained control over this ordering. It works like the Linux nice value: lower values mean higher priority.

Every (Cluster)ImageSetMirror and (Cluster)ReplicatedImageSet accepts a signed integer spec.priority field (default 0).

ValueBehavior
NegativeAlternatives 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.
PositiveAlternatives 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/v1alpha1
kind: ClusterImageSetMirror
metadata:
name: private-mirror
spec:
priority: -1
imageFilter:
include:
- .*
mirrors:
- registry: registry.example.com
path: /mirror
credentialSecret:
name: registry-secret
namespace: kuik-system

With 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.

ValueBehavior
0 (default)Default position; YAML declaration order is preserved among items at priority 0.
PositiveSorted ascending: lower value = higher priority.

Example: prefer Quay over ECR over Docker Hub:

apiVersion: kuik.enix.io/v1alpha1
kind: ClusterReplicatedImageSet
metadata:
name: nginx-unprivileged
spec:
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-unprivileged

For a pod requesting docker.io/nginxinc/nginx-unprivileged:1.29, this produces the following alternative order:

  1. docker.io/nginxinc/nginx-unprivileged:1.29 (original image, at CR priority 0)
  2. quay.io/nginx/nginx-unprivileged:1.29 (intra-priority 10)
  3. public.ecr.aws/nginx/nginx-unprivileged:1.29 (intra-priority 20)
  4. docker.io/nginxinc/nginx-unprivileged:1.29 (intra-priority 30, deduplicated with original so it will not be checked)

Both levels compose naturally. The full sort key is:

  1. CR priority (spec.priority) — ascending
  2. Type order — ClusterImageSetMirror > ImageSetMirror > ClusterReplicatedImageSet > ReplicatedImageSet
  3. Intra-CR priority (mirrors[].priority / upstreams[].priority) — ascending
  4. 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 original
apiVersion: kuik.enix.io/v1alpha1
kind: ClusterImageSetMirror
metadata:
name: global-mirror
spec:
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 original
apiVersion: kuik.enix.io/v1alpha1
kind: ImageSetMirror
metadata:
name: team-mirror
namespace: my-app
spec:
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-secret

For a pod in namespace my-app requesting docker-registry.example.com/my-app/api:v2, the resulting order is:

  1. fast-registry.internal/my-app-cache/my-app/api:v2 (CR priority -10, intra-priority 1)
  2. harbor.example.com/my-app-mirror/my-app/api:v2 (CR priority -10, intra-priority 5)
  3. harbor.example.com/global-mirror/my-app/api:v2 (CR priority -1)
  4. docker-registry.example.com/my-app/api:v2 (original image, priority 0)

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/v1alpha1
kind: ClusterReplicatedImageSet
metadata:
name: bitnami
spec:
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.

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.