Skip to content

Zone Retriever and cascade

A zone is driven by one document: its Retriever. It lists, by name and version constraint, the recipes the zone should hold, and names the cookbook to resolve them from. The instance re-reads it at every synchronization; changing what a zone contains means changing this document — or publishing a new recipe version that an existing constraint already covers. The document format is normative on the recipe-spec site: see the Retriever specification (the Retriever kind, recipe.tobby.dev/v1alpha1). A complete commented example ships in the repository as examples/retriever.yaml.

retriever.source accepts three forms (FR-010):

Form Example When
Local file /etc/tobby/retriever.yaml The document is managed with the instance’s configuration.
HTTP(S) URL https://git.example.com/platform/retriever.yaml The document lives in a Git repository or any web server — the common GitOps arrangement.
OCI reference oci://registry.example.com/config/retriever:v1 The document travels like the content it describes — including across zones, carried by Tobby itself.

The configured source is reported as configured on the Retriever administration screen (/admin/retriever, admin role) and its API mirror GET /api/v1/retriever — alongside the declared relaxed trust scopes and the effective synchronization interval. The interval override lives on the same screen (PUT /api/v1/retriever/interval, and DELETE to return to the configured value); it persists in the state directory, survives restarts, wins over sync.interval, and is audited as a sensitive configuration change (FR-094).

The retriever administration screen: configured source, interval and its runtime override

At each cycle the instance resolves every listed recipe from the cookbook, verifies its signature against the configured trust roots (FR-033), and reconciles. Version constraints are resolved at each pass; if no published version satisfies a constraint, that entry fails and says so — the other entries carry on. One unpublishable recipe never blocks the zone.

The cascade: connected → restricted → more restricted

Section titled “The cascade: connected → restricted → more restricted”
Zone A — upstream Zone B — downstream Zone C — further down Tobby re-verifies on entry Tobby re-verifies on entry Tobby re-verifies on entry zone registry …/docker.io/bitnami/wordpress zone registry …/docker.io/bitnami/wordpress zone registry …/docker.io/bitnami/wordpress recipes recipes The same signed recipes flow down unmodified — each instance re-verifies against its own trust roots Relocated paths are invariant: the same …/docker.io/… path in every zone, however many hops

Real topologies chain zones. The upstream zone promotes into its registry; the downstream zone’s Tobby fetches from that registry even though the recipes — immutable, signed, bit-exact — keep naming the origin hosts (docker.io/...). The bridge is source substitution (FR-036):

# Downstream instance
registries:
substitutions:
docker.io: registry.upstream.example
ghcr.io: registry.upstream.example
retriever:
source: oci://registry.upstream.example/cookbook/retriever:v1

Substitution changes only the network endpoint contacted — never the computed destination path (FR-035). That invariance is what makes the cascade compose: docker.io/bitnami/wordpress relocates to <registry>/docker.io/bitnami/wordpress in every zone, however many hops it crossed, and never degenerates into reg.zone2/reg.zone1/docker.io/.... Each zone’s registry holds the same relocated paths under its own host, and each zone’s cookbook — populated by recipe propagation (FR-034) — is what the next zone’s Retriever points at. The rule and its rationale are ADR-0013; the normative grammar (host canonicalization, port encoding, substitution semantics) is RECIPE-SPEC §11.5.

A substitution maps an origin host to a base. The instance then appends the full canonical path — origin host included:

effective = <base>/<canonical-host>[_<port>]/<repository-path>
substitutions:
docker.io: registry.upstream.example
docker.io/library/nginx → registry.upstream.example/docker.io/library/nginx
ghcr.io/dexidp/dex → ghcr.io/dexidp/dex (not substituted: as written)

The origin host stays in the path on purpose: that is where an upstream Tobby holds the content, so the downstream instance finds it without any translation. The value is therefore a host — plus, at most, the base path the upstream promoted under — and never repeats the origin host.

What to write depends on what fills the upstream registry:

Upstream registry Filled by Substitution value What the operator does
The embedded registry (/v2/) of an upstream Tobby That instance’s synchronizations <upstream host>, or <upstream host>/<storage.basePrefix> when one is set Nothing: it already holds the canonical layout.
A zone registry an upstream Tobby promotes into destination.registry <registry host>, or <registry host>/<destination.basePath> when one is set Nothing beyond the promotion itself: Tobby pushes docker.io/library/nginx to <registry>/docker.io/library/nginx. On Harbor, the top-level project named after the origin host must exist (ADR-0013).
A pull-through proxy: Harbor proxy-cache project, Artifactory or Nexus remote repository The proxy itself, on demand, from the origin <proxy host> Create one proxy per origin host, named exactly after the canonical host (below).

The three cases converge on the same layout. A zone registry fed by an upstream Tobby gets it for free, because Tobby fetches from the origin without substitution and pushes under the origin prefix. A proxy gets it from its name.

Pull-through proxies: one project per origin host

Section titled “Pull-through proxies: one project per origin host”

A pull-through proxy rewrites the other way round: its project name replaces the origin host. A Harbor proxy-cache project named dockerhub serves harbor.example/dockerhub/library/nginx from Docker Hub’s library/nginx. A substitution pointed at that project would ask Docker Hub for docker.io/library/nginx, which does not exist.

The fix is the proxy maintainer’s, and it is only a matter of naming. Name the project after the canonical origin host. The path Tobby asks for is then exactly the one the proxy expects:

Tobby asks harbor.example/docker.io/library/nginx
Harbor reads project "docker.io", repository "library/nginx"
Harbor fetches library/nginx from Docker Hub

The canonical host is at once the substitution key and the project name, and the path asked of the proxy is always <proxy>/ followed by the recipe’s ref (a port written _<port>):

Origin registry Substitution key / proxy project Recipe ref
Docker Hub docker.io docker.io/library/nginx
GitHub Container Registry ghcr.io ghcr.io/dexidp/dex
Quay quay.io quay.io/jetstack/cert-manager-controller
Kubernetes registry.k8s.io registry.k8s.io/kube-state-metrics/kube-state-metrics
Google Container Registry gcr.io gcr.io/distroless/static
Google Artifact Registry europe-docker.pkg.dev europe-docker.pkg.dev/acme/apps/api
Microsoft Artifact Registry mcr.microsoft.com mcr.microsoft.com/dotnet/runtime
Amazon ECR Public public.ecr.aws public.ecr.aws/docker/library/alpine
Azure Container Registry acme.azurecr.io acme.azurecr.io/team/app
GitLab.com registry.gitlab.com registry.gitlab.com/acme/app/api
Self-hosted forge on a custom port key forge.example.com:5050, project forge.example.com_5050 forge.example.com:5050/acme/app
  • Docker Hub is the only special case, and only for clients. docker pull nginx means docker.io/library/nginx. A recipe always writes the host, and library/ for official images. index.docker.io and registry-1.docker.io fold into docker.io (a closed list, ADR-0013), so the single project docker.io covers all three spellings.
  • A custom port. This is common on self-hosted forges such as GitLab or Froggit, whose registry often listens on 5050. The key keeps the : (forge.example.com:5050). The path encodes it as _ (forge.example.com_5050), because a repository name cannot contain :. The proxy project is therefore forge.example.com_5050.
  • One host, one project. This holds even for the same vendor: gcr.io and us.gcr.io, or two Artifact Registry regions, are two projects.
  • Check the names. They contain dots, and an underscore when a port is involved. Check that your proxy accepts them before building on this layout.
  • Trust does not move. The proxy authenticates to the origin with its own credentials. Tobby authenticates to the proxy: the credentials file and the allowlist name the proxy host. The proxy is no trust anchor either: signatures and pinned digests are verified exactly as if the bytes came from the origin.

Two policies deliberately read different references:

  • The registry allowlist (FR-030) and credential lookup apply to the effective host actually contacted — the substitute. That is where the bytes come from, so that is what network policy must name.
  • Trust-root scopes (FR-033) match the nominal ref written in the recipe. Signed provenance does not change because content was fetched from a closer copy.

Logs record the nominal→effective mapping on every substituted fetch, so an audit can always answer both “what was this content” and “where did these bytes come from”.

A downstream instance authenticates to its upstream like any registry client. Create a dedicated account (or token) on the upstream instance — see authentication and RBAC — with read as its only need, and hand it to the downstream instance through its credentials file (FR-004):

registries:
credentialsFile: /etc/tobby-credentials/config.json

The file is a standard dockerconfigjson payload; entries are looked up by the effective host, so the entry names the upstream registry — registry.upstream.example — not docker.io. It must live outside the store (secrets never travel on transportable media), and on Kubernetes it is a mounted Secret the chart wires for you. Write-scoped credentials follow the same path: pushing to the zone’s destination registry uses the same credentials file, keyed by the destination host.

The destination side of the same instance is its own configuration section — destination.registry, plus destination.basePath and destination.cookbook — deliberately separate from substitutions: one answers “where do I read from”, the other “where do I promote to”, and applying a read-side rewrite to a write would publish to a registry nobody named.

Next: your clusters and hosts consume what landed — connect your clients.