Fleet OCI storage: multiple registries and per-cluster routing

UI prototype and design notes for rancher/fleet 5711, which covers 4843 and 4844. Built from f3dcae8ae on 2026-11-04, on top of dashboard master, proxying the mf Rancher.

Where this stands

Try it

  1. Open the prototype and log in with your mf Rancher account. The sample data is in workspace fleet-default. The page is at Continuous Delivery, Resources, OCI Storage.
  2. Debug a failure: Clusters tab, click edge-store-0203.
  3. See replication wait: Cluster Routing, open apac-clusters, set Replication wait to 2. Effect on Clusters now predicts Failover. Save it and check the Clusters tab.
  4. See route order matter: Add Route, pick eu-staging-01 and any registry. The form warns that eu-clusters matches it first. Save, then move it up with the arrows.
  5. See the simple case: switch the workspace to fleet-local and add one registry.
  6. Other pages touched: App Bundles, Create, Git Repo, Advanced step. A Fleet cluster's OCI Storage tab. A workspace's Default OCI Registry tab.
  7. Reset mock data in the yellow banner puts everything back.

The problem

Fleet stores a bundle in one registry and every target cluster pulls from that same place. That breaks when the management cluster and downstream clusters can't use the same registry, namespace or credentials, or when a site needs failover. The RFEs ask for per-cluster registry, credentials and namespace (4844) and for multiple registries with failover (4843). The triage comment on 4843 sets two constraints: mirroring stays out of Fleet's scope, and agents must retry with back-off when an artifact hasn't replicated yet.

What Fleet does today, which shaped the design:

The model the UI assumes

GitRepo fleet apply harbor-central source: Fleet pushes here push harbor-eu mirror harbor-apac mirror zot-stores mirror in each store your replication (Harbor rules, etc.), not Fleet eu-prod-* harbor-eu, then harbor-central apac-prod-* harbor-apac, then harbor-central edge-store-* zot-stores only agent pulls failover when the first registry doesn't work

Use cases

WhoNeeds toIn the prototype
Platform adminTurn OCI storage on with one registry, like todayAdd Registry. The first one becomes the source and the default route. No routing to configure.
Platform adminSend each region to its own registry, with its own namespace and credentialsRegistries, then Cluster Routing. A route picks clusters by name, cluster group or label.
Platform adminKeep deploying when a regional mirror is downA route's Pull Order. The cluster shows Failover and names what failed.
Edge adminServe air-gapped sites whose registry the management cluster can't reachRoute by cluster group, and Test Connection from a cluster instead of the controller.
App ownerDeploy without knowing the topology, or pin one workload to a registryGitRepo form, Advanced step: follow workspace routing (default) or pin.
On-call"Why is this bundle stuck on this cluster?"Clusters tab and its drawer, or the OCI Storage tab on the Fleet cluster page: state, reason, next step.
On-call"Did the last push reach everywhere yet?"Artifacts tab: per registry and per cluster.
Anyone changing routing"What happens if I save this?"Effect on Clusters in the route form: chain before and after, expected state, routes that shadow this one.
Security reviewWhich clusters get which credentials? Is TLS off anywhere?Agent Access column, and warnings when agents would get push credentials or TLS is off.

Walkthrough

Registries. The summary and banner answer "is anything wrong?" first. A row distinguishes a registry that is down from one that a single cluster can't reach, and warns when TLS verification is off.
Registries. The summary and banner answer "is anything wrong?" first. A row distinguishes a registry that is down from one that a single cluster can't reach, and warns when TLS verification is off.
Cluster Routing. An ordered list with a default route at the end. Expanding a route shows its clusters, and each opens the same drawer as the Clusters tab.
Cluster Routing. An ordered list with a default route at the end. Expanding a route shows its clusters, and each opens the same drawer as the Clusters tab.
Clusters: the effective configuration, which is what 4844 asks for. Which registry, which namespace, which credentials, problems first. A failing cluster still shows the registry it is trying. Waiting is blue on purpose: an agent retrying is not an error.
Clusters: the effective configuration, which is what 4844 asks for. Which registry, which namespace, which credentials, problems first. A failing cluster still shows the registry it is trying. Waiting is blue on purpose: an agent retrying is not an error.
Cluster drawer. What failed, why, what to do next, and links to the route and registry involved.
Cluster drawer. What failed, why, what to do next, and links to the route and registry involved.
Artifacts. Did the last push reach everywhere? harbor-apac is 4 minutes behind and two clusters are waiting. Expanding a bundle shows each cluster.
Artifacts. Did the last push reach everywhere? harbor-apac is 4 minutes behind and two clusters are waiting. Expanding a bundle shows each cluster.
Route form. It reuses the cluster selector from the GitRepo form. Effect on Clusters previews the result before saving and catches a route that an earlier one shadows.
Route form. It reuses the cluster selector from the GitRepo form. Effect on Clusters previews the result before saving and catches a route that an earlier one shadows.
Registry form. Host and namespace are separate. Test Connection runs from a chosen cluster, because the controller often can't reach a mirror. The resulting Secret keeps today's type and keys.
Registry form. Host and namespace are separate. Test Connection runs from a chosen cluster, because the controller often can't reach a mirror. The resulting Secret keeps today's type and keys.
GitRepo form, Advanced step. App owners follow workspace routing by default, or pin to one registry, which is what setting <code>ociRegistrySecret</code> means today.
GitRepo form, Advanced step. App owners follow workspace routing by default, or pin to one registry, which is what setting ociRegistrySecret means today.
Real Fleet cluster page. The same panel as the drawer, as a tab. This cluster has no region label, so it falls through to the default route.
Real Fleet cluster page. The same panel as the drawer, as a tab. This cluster has no region label, so it falls through to the default route.
Empty workspace. No registry means bundles stay in etcd, as they do today.
Empty workspace. No registry means bundles stay in etcd, as they do today.

What data is worth showing

Exists Fleet has it today, though the dashboard reads neither contentsId nor ociReference yet. New needs a backend change. Derived the UI can compute it once the new data exists.

DataWhy a user wants itStatusSourcePriority
Registry
Host and namespaceWhere artifacts live. Customers think of these separately, so the form splits them.ExistsSecret referenceMust
Role: source or mirrorFleet needs to know where it may push. Users need to know what they must replicate.NewNew field or labelMust
Push and agent usernames, TLS modeSpot agents that hold push credentials, and registries with TLS off.ExistsSecret keys. Needs Secret read access, see ask 2Should
Clusters routed here, clusters pulling nowIs this registry used? How much breaks if it goes down?DerivedRoutes plus agent statusShould
State and failing clustersTells "registry down" apart from "one cluster has a network problem".DerivedAgent statusShould
Last push, last pullQuick signal that it is alive.NewBundle and agent statusNice
Artifact count and sizeCapacity planning. Size is why people use OCI storage at all.NewBundle statusNice
CA expiryWarn before a bundle silently stops being pullable.DerivedParse cacertsNice
Reachability from a given clusterThe controller often can't reach a mirror, so testing from it proves little.NewAgent-side probeNice
Route
Cluster selection, ordered registries, replication waitThe mapping both RFEs ask for.NewRouting resourceMust
Matching clustersPreview before saving, and see which route wins.DerivedUI can compute from labels and groupsMust
Cluster
Effective route and registry chainThe direct answer to "which registry, which credentials, which namespace".NewCluster.statusMust
Registry in use nowPrimary or failover?NewAgent-reportedMust
Result per registry, with a failure classAuth, TLS, unreachable, not found and integrity need different fixes.NewAgent-reportedMust
Last successful pullHow stale is this cluster?NewAgent-reportedShould
Config in use vs config wantedShows routing changes that haven't landed. See question 1, this may not be needed.NewGeneration or hashShould
Bundle and artifact
Artifact IDWhat to look for in the registry.ExistsBundle.spec.contentsIdShould
ReferenceToday it is a single value marked informational. It needs to be per cluster.ExistsBundle.status.ociReferenceShould
Per-cluster pull state, attempts, next retryTells a slow mirror apart from a broken one. Without it this reads as an error.NewBundleDeployment.statusMust
Availability per registryOnly known from agents: Fleet can't see inside a mirror it may not reach.DerivedAgent statusShould
Size, pushed atReplication lag needs a start time. Size explains slow pulls.NewBundle statusShould
Pull durationFinds a mirror that works but is slow.NewAgent-reportedNice

What the UI would need from the backend

  1. A place for routing with the fields in the first example below, wherever it ends up (options).
  2. Status on resources that aren't Secrets. Someone debugging a pull shouldn't need to read credentials. Aggregate on Cluster.status as well: the Fleet cluster page already loads every BundleDeployment, and that doesn't scale to a table of thousands of clusters.
  3. A distinct, non-error state for "waiting for replication". Fleet recently added WaitingForDependency, and the dashboard needed fixes because it painted the new state red. Emit it in status.display.state and in the summary counts from day one, and we add it to the UI state tables alongside.
  4. A failure class, not only a message. Unauthorized, TLS, Unreachable, NotFound and IntegrityMismatch. The agent needs it to decide between failing over and backing off anyway, and the UI uses it for the state and the next step.
  5. Artifact metadata on the Bundle: size and pushed-at. Without pushed-at nobody can see replication lag.
  6. Later: an agent-side connectivity probe behind Test Connection. Today that button is simulated.

The field names below are only there to make the idea concrete:

apiVersion: fleet.cattle.io/v1alpha1
kind: OCIStorageRouting            # hypothetical name
metadata:
  name: default
  namespace: fleet-default
spec:
  routes:                          # first match wins
    - name: eu-clusters
      clusterSelectors:            # same vocabulary as GitRepo targets
        - matchLabels: { region: eu }
      registries: [harbor-eu, harbor-central]   # existing OCI storage secrets
      replicationTimeout: 10m
    - name: air-gapped-stores
      clusterGroups: [airgap-stores]
      registries: [zot-stores]
  default:
    registries: [harbor-central]
# BundleDeployment.status, reported by the agent (hypothetical)
ociPull:
  registry: harbor-apac
  state: WaitingForArtifact        # Pulled | WaitingForArtifact | Failed
  reason: NotFound                 # Unauthorized | TLS | Unreachable | NotFound | IntegrityMismatch
  attempts: 3
  nextRetry: "2026-11-04T16:21:00Z"
  lastSuccess: "2026-11-04T10:12:00Z"

# Fleet Cluster.status, aggregated so the UI doesn't have to load every BundleDeployment
ociStorage:
  route: apac-clusters
  chain: [harbor-apac, harbor-central]
  active: harbor-apac
  waiting: 1
  failed: 0

Where routing could live

The first three are from the issue. The UI only depends on "registries, routes, status", so changing this later only touches the prototype's data layer.

OptionGoodProblem for users
Field on the Fleet ClusterEasy to explain. Per-cluster view is trivial.One edit per cluster. A new cluster uses the fallback registry until someone sets it, and its bundles fail if it can't reach that one. A failover chain needs a list anyway.
Target customizationsPer-app flexibility.App owners need to know the registry topology. The same mapping is copied into every repo. No single place shows where a cluster pulls from.
New secret structure, or a separate ocistorageForClusters secretNo new CRD. Same idea as helmSecretNameForPaths.Routing sits inside a Secret, so seeing it requires Secret read access, which a read-only user or on-call engineer may not have. No status to report on, no field validation, and the UI has to parse and rewrite a blob.
New routing resource that references today's secrets (what the prototype assumes)Credentials stay in Secrets. Routing is readable with normal RBAC, can be validated, and has its own status. Existing secrets keep working.A new CRD to own and migrate to.

My recommendation from the UI side is the new resource. The secret option can work if a new CRD is a hard no, but then status on Cluster and BundleDeployment matters even more, because the routing itself won't be readable for many users. A per-cluster override doesn't need its own field: it's a route that names one cluster (lab-sandbox in the sample data).

Open questions

For the Fleet team

  1. Should a routing change need an update at all? The prototype follows today's rule (changes apply on the next Git update or force update) and shows Update Pending. But that delay comes from the per-bundle copy of the settings. If the controller reads the current route when it clones credentials, a routing change could apply on the next pull, and nothing needs redeploying because artifact IDs are content-addressed. That would remove a state, a banner and a checkbox from this UI. I'd prefer that.
  2. Is the replication wait per route or global? The prototype has it per route because a store on a slow link differs from a region. A single default may be enough to start with.
  3. What cleans up mirrors? Fleet deletes old artifacts from the source (apply.go). Mirrors only lose them if the replication tool propagates deletes.
  4. Hundreds of sites with their own registry. One entry per host doesn't scale. The sample data avoids it with a local DNS name that every store shares. If that isn't realistic, the reference may need to template on cluster labels.
  5. Does Fleet ever push to more than one registry? The prototype assumes one source, following the triage comment. Sites with no replication tool would have no way to fill a mirror.

For UX

  1. Words: Source and Mirror, Route, Pull Order, Waiting. Do these read well to someone who isn't a registry expert?
  2. Placement: it sits under Resources next to Bundles because OCI storage is opt-in. Top level would be easier to find. Should the Clusters tab also be a column on the main Fleet Clusters list?
  3. Banners: a page in trouble can show the prototype banner, a failure banner and a pending-update banner together. Without the prototype banner it's two, and one goes away if question 1 goes the way I'd prefer.
  4. The workspace's Default OCI Registry tab overlaps with "source". It prefills the GitRepo form through a UI-only annotation. I'd drop it once a source exists. The registry form could also serve dashboard issue 14433 (create OCI secrets from the workspace form), which is labelled requires-prototyping.

What the prototype doesn't do