Skip to content

Architecture

Reference-only inventory. One CRD: namespaced Image.

The controller answers which image refs are referenced in this namespace, and by which kinds — templates, naked Pods, and ephemeral containers on owned Pods. Not running counts, digests, or object names.

Find concrete objects with kubectl (get deploy,sts,ds,job,cronjob,pod). Inventory carries kinds only.

System overview

flowchart TB
  subgraph NS["Namespace"]
    D["Deployment / StatefulSet / DaemonSet"]
    CJ["CronJob / Job"]
    PO["Pods"]
  end

  subgraph CTRL["Image Inventory Controller"]
    Q["Work queue<br/>key = namespace / _"]
    R["reconcileNamespace<br/>full NS scan"]
    E["ensureImage + GC"]
  end

  CR["Image CRs<br/>spec.image · status.kinds"]

  D -->|generation change| Q
  CJ -->|generation change| Q
  PO -->|spec / owners / delete| Q
  CR -->|create / delete only| Q
  Q --> R
  R --> E
  E --> CR

Reconcile model

Events do not patch a single Image. Every watch enqueues the namespace. One reconcile rebuilds that namespace’s desired set, then creates, updates, or deletes managed Images.

sequenceDiagram
  participant W as Watch
  participant Q as Work queue
  participant R as Reconciler
  participant API as API server

  W->>Q: enqueue namespace/_
  Note over Q: Coalesces Pod bursts into one NS rebuild
  Q->>R: Reconcile(namespace)
  R->>API: List Deploy/STS/DS/CronJob/Job/Pod
  R->>R: Aggregate normalized ref → kind set
  R->>API: List managed Images
  loop each desired ref
    R->>API: CreateOrUpdate Image + status.kinds
  end
  R->>API: Delete managed Images not in desired set
Choice Why
Namespace-keyed queue (name: _) Pod burst → one rebuild
Full NS scan Simple correctness; no delta graph
No primary For() All types use Watches → NS enqueue
Image updates ignored Avoid loops on own status writes

Watched: Pod, Deployment, StatefulSet, DaemonSet, Job, CronJob, Image (create/delete only). Not watched: ReplicaSet — workload templates already supply images and kind. No Pod→RS→Deploy walk.

Target Predicate
Pod Container / init / ephemeral / owners / deletionTimestamp
Deploy / STS / DS / Job / CronJob GenerationChangedPredicate
Image Create + delete; updates ignored

Sources → status.kinds

flowchart TD
  W[Workload] --> T[Pod template / JobTemplate]
  T --> I[ImagesFromPodSpec]
  I --> K[Credit workload kind]

  P[Pod] --> N{ownerReferences empty?}
  N -->|yes naked| A[All containers + init + ephemeral]
  A --> KP[Credit Pod]
  N -->|no owned| E[Ephemeral only]
  E --> KP

  J[Job] --> CJ{CronJob ownerRef?}
  CJ -->|no| KJ[Credit Job]
  CJ -->|yes| L{live CronJob same name?}
  L -->|no| KJ
  L -->|yes| U{UID empty or matches?}
  U -->|yes| SKIP[Skip Job]
  U -->|no| KJ
Source Images from Kind
Deployment / StatefulSet / DaemonSet template: containers + init (+ ephemeral if present) that kind
CronJob jobTemplate pod template CronJob
Job pod template; skipped if live CronJob same name and owner UID empty or matching Job
Naked Pod containers + init + ephemeral Pod
Owned Pod ephemeral only (kubectl debug) Pod

Rules:

  • Scale-to-0 still counts (template still references the image).
  • Owned Pod template images do not add Pod; parent workload already does.
  • CronJob > Job when a live CronJob with the same name exists and the ownerRef UID is empty or matches; otherwise Job is credited (stale UID → both kinds possible).
  • Objects with deletionTimestamp contribute nothing.

Image identity

  1. Normalize the ref (internal/imageutil).
  2. CR name = DNS-1123 slug + short hash of normalized ref.
  3. One Image per (namespace, normalized ref). Same ref in two namespaces → two CRs.

status.kinds is a comma-separated sorted string (e.g. Deployment,Pod) so kubectl columns render. See API.

Garbage collection

Only Images labeled app.kubernetes.io/managed-by=image-inventory-controller participate.

After rebuild: delete managed Images not in the desired set.

  • Without the label → left alone.
  • With the label → adopted: spec is overwritten; orphans are garbage-collected.

Runtime

Concern Behavior
--namespaces Comma-separated filter for cache and reconcile. Empty = all namespaces. Outside the filter, Images are not updated. Shipped RBAC is a ClusterRole.
Leader election --leader-elect with lease in --leader-elect-namespace (default image-inventory-system; must match RBAC).
Metrics Shipped Deployment binds 127.0.0.1:8080.
NetworkPolicy Included in the default install bundle.

Scope

In: referenced images from watched templates + naked/ephemeral Pods; kinds-only status; namespaced CRs; managed-by GC.

Out: live podCount, digests, object-name graphs, ReplicaSet watching, cluster-scoped Image, registry/CVE/SBOM, kubectl plugins.