Skip to content

docs(design): bring your own cluster and per-cluster state - #1288

Draft
leninmehedy wants to merge 20 commits into
mainfrom
01287-design-bring-your-own-cluster
Draft

leninmehedy wants to merge 20 commits into
mainfrom
01287-design-bring-your-own-cluster

Conversation

@leninmehedy

@leninmehedy leninmehedy commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Design doc for #1287. No code changes.

IMPORTANT: This external cluster support is for provisioner test only feature. We don't encourage anyone to use it in their CI or cluster

Summary

Please review

  • Section 5: Identity checks
  • Section 5.5: the lease design.

Refs #1287

Add a design for proving a cluster was installed by us before trusting
it, for using an operator-supplied cluster through the Kubernetes API
only, and for keeping state per cluster under state/hosts/local and
state/clusters/<uid>.

Covers the identity gate, the state layout and migration, API-only
external mode, local development with Kind, and the effect on CI lanes.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
@leninmehedy
leninmehedy requested a review from a team as a code owner October 15, 2026 06:22
@swirlds-automation

swirlds-automation commented Oct 15, 2026 •

Copy link
Copy Markdown

✅ Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
✅ Open Source Security 0 0 0 0 0 issues
✅ Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

…install

Describe the state layout in terms of the per-component files from #1231
and #1245: hosts/local holds all component files, and clusters/<uid>
holds only the cluster-scoped ones. Rewrite the migration as a move of
component files that runs after the per-component split.

List the component files that mix host and cluster fields, archive
cluster state at uninstall, and make a stale-state install stop and ask
the operator to clean up instead of moving state automatically. Order
the delivery plan after #1235.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Add a Lease per component per cluster, in the component's namespace,
taken by mutating commands in both provisioned and external mode so two
operators cannot change the same component at once. Build it on client-go
leaderelection, wrapped in internal/kube/lease, with fail-fast holder
details and context cancellation when the lease is lost.

Add it to the goals, decisions, delivery plan and open items.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
…nd field guard

Add a small hosts/local/host.yaml identity file (name, machine ID, the
cluster this host provisioned) that the identity gate reads first, and a
global provisioner.yaml for the provisioner and state model versions, so
a machine with only external clusters (for example a Mac) still has a
home for the data startup migrations depend on. Move those fields out of
the host-level system file proposed in #1245.

Drop the legacy backfill in favour of a catalog checksum check, explain
the external-then-local case and how a cluster is identified to a human,
and show the layout with the profile, firewall and system files. Rewrite
the host-bound field section: same component files in cluster
directories, a writer guard for host-only fields, and the profile kept
with its target.

Add non-goals for the cluster-side lock, and tidy the migration risks,
delivery plan, decisions and open items to match.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
…ate work

Describe how the hosts/local migration builds on #1235's component path
helpers, runs under its startup-migrations lock, and leaves the
state.yaml.legacy and state.yaml.diverged-* files untouched.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Add `kube cluster connect` as the one command that registers a cluster
(test the connection, read the cluster ID, check permissions, bind the
alias), with `list` and `info`, and implicit registration for
non-interactive runs. Exempt `connect` and `list` from the identity gate.

Add a prior-art section on hiero-ledger/solo (alias to context map,
in-cluster state ConfigMap and validator, lease) with what we use and
what we avoid, and note the in-cluster state shape and cluster grouping
as follow-ups.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
State that a cluster passing the provisioned proof registers without
`kube cluster connect` (at install for new clusters, on the gate's first
successful pass for older installs), so the unregistered-cluster rule
applies only to external clusters. Describe `connect` as an optional
eager registration for older installs.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
State what the design gives CI (fewer KVM VMs, isolated parallel jobs,
no privileged runners for most tests, external mode tested on every PR)
and what it does not cover (host-level tests, the tag split, block node
storage). Give the order: record a baseline, start the Kind lane with
API-only tests, add block node, and build the host-level lane in
parallel.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
…ters

Make KVM a nightly job that a maintainer enables by hand for a PR that
needs a real VM, instead of starting it from path filters. Path filters
may later suggest it but do not start it.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Replace the manual-only wording: KVM starts automatically when a PR
touches files only a real VM can check (using the existing paths-filter
job), by hand through a dispatch or label for anything else, and nightly
as a safety net. Add the starting candidate paths and the cost trade-off
of a broad filter.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
…ommand needs

Separate local evidence, recorded identity (cluster ID, server address
and CA fingerprint) and live identity, so a broken local cluster is
still recognised as ours and never treated as external. Replace the
overlapping outcome table with ordered rules that cover a wrong machine
ID, a rebuilt cluster, an unreachable API and unreadable evidence.

Replace the exempt command list with a per-command need (none, connect,
install, host, api, host+api), so uninstall and the host-level commands
work on a broken cluster. Record the identity on the first successful
pass for older installs, and add a threat-model note.

Refs #1287

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Clarify that only api/host+api needs run the full outcome table and the
host need is an offline-only check. Match recorded identity on the CA
fingerprint, with the server address as fallback only. Give provisioner.yaml
a cli and daemon section so each binary records its own version and both
detect skew. Note that fencing needs ctx-cancellable steps and that the
gate reads state through the #1235 loader.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Reshape the section 5.2 table into a precondition plus three branches
(target matches recorded identity, nothing recorded, not our cluster),
so the "first match wins" precedence and the split-brain reasoning read
off the structure. Stable rule numbers are kept for cross-references.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Comment thread docs/dev/bring-your-own-cluster.md Outdated

1. Never trust a cluster as ours unless we can prove we installed it on this machine. Check this as early as possible, and stop if the proof fails.
2. Let an operator explicitly point the CLI at any cluster (`--kubeconfig`) and use it for API-level work: install, upgrade and uninstall of block node, consensus node and the other cluster components.
3. Keep state separate per cluster, so switching between clusters, or forgetting a flag, can never mix or reset state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given solo-provisioner is a tool to be run in a single node cluster, should we avoid the complexity of running with multiple clusters? I see the benefit in trying to parallelise tests by using multiple clusters, but I wonder how worth it those tests would be if they are all sharing the same state file.

@leninmehedy leninmehedy Oct 16, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

given solo-provisioner is a tool to be run in a single node cluster

This assumption is now relaxed a bit in the sense that someone can deploy resources in remote/external/Kind clusters with no access to the underlying bare-metal (gated under --experimental feature). This is primarily for various testing scenarios in local-dev/CI/CITR so on. We are adding proper external cluster support (with locks and in-cluster audit records) rather than leaving a risky loophole (currently available) or closing it altogether.

if they are all sharing the same state file

When we use multiple clusters, the state files will be separate for each cluster (state managedment will be refactored to support this). So they are not sharing the same state file.

/opt/solo/weaver/state/

└── clusters/
    ├── <cluser-1-uid>/        
    │   ├── cluster.yaml           # cluster-scoped components only
    │   ├── blocknode.yaml
    │   ├── consensus.yaml
    │   └── teleport.yaml
    ├── <cluser-2-uid>/        
     │   ├── cluster.yaml           # cluster-scoped components only
    │   ├── blocknode.yaml.    # cluster-2 has blocknode only
    └── index.yaml                 # complete view of every known cluster

Please let me know if you see any other issues

Comment thread docs/dev/bring-your-own-cluster.md Outdated
…experimental

Add a top banner and a decision making external mode (--kubeconfig against
a cluster weaver did not provision) a provisioner-test-only feature, not for
production and not encouraged in operators' CI or clusters. Reuse the existing
common.GateExperimental gate (feature external-cluster): external mode and
kube cluster connect require --experimental or SOLO_PROVISIONER_ENABLE_EXTERNAL_CLUSTER
and are refused otherwise. Reframe goal 2, section 7, the Kind section and the
CI section to match.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
@leninmehedy
leninmehedy requested a review from brunodam October 16, 2026 01:20
@alex-au

alex-au commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Thanks Lenin for another great design, a few thoughts:

  • Identity gate (Sections 5 and 8): The gate needs persisted state (binary hashes, cluster ID, etc.) for verification, but it's supposed to run before state loading. Could we clarify the startup order, including migrations?

  • Verification failures (Sections 5.2, 5.3): What happens if we can't determine the cluster identity because the API is unreachable? Also, could a failed identity check block recovery or uninstall when the local kubelet is broken?

  • External state (Section 12.3): Can we really treat all external state as a disposable cache? Some fields may represent desired state or information we can't recover from the cluster. We should probably keep the same distinction between missing and unobservable resources that we introduced in Out-of-band drift detection framework (reality-vs-persisted diff + report) #1193.

  • Resource ownership (Section 7): Cluster identity tells us which cluster we're connected to, but not which resources Weaver owns. How do we handle existing Helm releases or other resources that weren't created by Weaver, especially during uninstall?

"Identity" was overloaded across the host, the cluster, the recorded
host-to-cluster binding and the target. Rename the gate to the provenance
gate and adopt one noun per thing: HostIdentity (machine-id), ClusterIdentity
(kube-system UID), provision record (what this host recorded about the cluster
it provisioned), Target, and Origin (the provisioned/external verdict; a refusal
is a stop, not an origin). Add a Terms table to section 5.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Add section 5.0 (startup order): the three state tiers, the full pre-run
sequence, and why the provenance gate is pinned between the filesystem
migrations it depends on and the cluster migrations that depend on it.

Add section 8.1 (startup migrations split around the gate): ScopeStartup
stays filesystem-only, a new ScopeStartupCluster runs cluster-touching
migrations after the gate (provisioned-only, skipped in external mode), and
the three Cilium migrations move there.

Add a FAQ (section 15) covering the recurring questions: gate-after-migrations,
no cluster work in ScopeStartup, not a security boundary, provenance vs
identity, the narrow-only allowed-origins allowlist, external mode being
test-only, behaviour with no cluster, and how commands opt in.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
…onal

Rule ids (P.1, A.4, ...) appear in logs, hints and the doc, so a new rule takes
the next free number in its branch and keeps its evaluation position in the
table rather than renumbering existing ids. The numeric suffix is allocation
order, not evaluation order.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
@leninmehedy

leninmehedy commented Oct 16, 2026 •

Copy link
Copy Markdown
Member Author

Thanks @alex-au for the feedback. Below are my responses to your questions.

  • Identity gate (Sections 5 and 8): The gate needs persisted state (binary hashes, cluster ID, etc.) for verification, but it's supposed to run before state loading. Could we clarify the startup order, including migrations?

Good question. I have renamed Identity -> Provenance since Identity term is overloaded.

  1. Provenance gate doesn't need to go through BLL/RSL. BLL/RSL needs a verified cluster/host. so gate needs to run before a verified target (HostIdentity, or ClusterIdentity) is determined for the given command. Each command would define it's need (host vs k8s api or host+api) and based on that the gate would ensure the need is met before we go into the intent handling by BLL.

  2. For the sort order I had to do a small coding spike (feat(provenance): add cluster provenance gate and wire block node install #1298) to see where we get blocked in the startup order. What I found is that the start-up migrations (ScopeStartup) are mixed with filesystem + cluster/host level migrations. We need to define new types - ScopeStartupFilesystem, ScopeStartupCluster, ScopeStartupHost. So the Provenance Gate needs to run after the filesystem startup migrations (ScopeStartupFilesystem), but before the cluster/host level migrations.

flowchart TD
    cmd["Command, e.g. block node install<br/>declares need: host+api"] --> prerun["Shared pre-run<br/>root PersistentPreRunE"]
    prerun --> fsmig["Filesystem migrations<br/>ScopeStartupFilesystem"]
    fsmig --> rpg["RunProvenanceGate<br/>common helper"]
    rpg --> collect
    subgraph prov ["internal/provenance"]
        collect["defaultCollector.Collect<br/>reads host + cluster into Input"] --> gate["Gate(need, Input)<br/>need policy check"]
        gate --> eval["Evaluate(Input)<br/>P.1 then branch A/B/C"]
        eval --> allow["Allowlist (modes.go)<br/>narrow only"]
    end
    gate -. policy stop .-> stop
    eval -. rule stop .-> stop
    allow -->|stop / not allowed| stop["Stop: doctor message + hint"]
    allow -->|external| clustermig["Cluster migrations<br/>ScopeStartupCluster (api / host+api)"]
    allow -->|provisioned| hostmig["Host migrations<br/>ScopeStartupHost"]
    hostmig --> clustermig
    clustermig --> bll["BLL handles the intent<br/>command proceeds"]
Loading

I have updated the design doc based on my current findings/understanding. We may discover few more ambiguity as we implement, but it is hard to predict all such ambiguity unless we get into the trench.

One more thought, this design doc gives us a north star, but it is not a full proof solution doc yet. Rather, we need to keep it as a live design doc and update as we identify new blockers. I have added a FAQ section so that we can track various questions and decisions/answers that helped us to evolve it.


  • Verification failures (Sections 5.2, 5.3): What happens if we can't determine the cluster identity because the API is unreachable? Also, could a failed identity check block recovery or uninstall when the local kubelet is broken?

Current thought is that we trigger error and stop, just keep it simple in the first MVP. We then let the user determine what to do since we cannot recover from such error automatically.

Scenario Action
reality matches cache proceed
reality confirmed different (resource gone, or value changed) drift → stop and ask the operator (overwrite cache, or investigate)
reality unobservable (API down, RBAC denied, timeout) error and stop — never assume “gone”

Commands that need to observe the cluster (api / host+api) would block if API is down. It must not block the offline recovery/uninstall commands (need = 'host') — those don’t observe the cluster, so a down API shouldn’t stop them.


I don't have a concrete idea on how to differentiate desired and cached state, it's complicated. Question would be where should we keep the desired state, ideally in a git repository (GitOps style) to track changes, but that's an overkill for solo-provisioner's current usecase.

Therefore, the provisioner state was originally designed as a cached state since that's easier to reason about, but with the tradeoff of keeping it in sync. Idea was that if all resources are managed through solo-provisioner, we can sync the cached state as our last known state. So the kubernetes cluster is the reality and it must match with our cached state (our last known state). Thus it helps us to detect the out-of-band drift and therefore we can ask user if we should overwrite cached state or stop for manual investigation.

state cache = weaver’s last-known-observed state
reality = the cluster and the host
drift = reality ≠ cache

Having said, that we can still support desired state in a remote Git repository later if we want, unless other GitOps solutions are not enough.

Please let me know if I missed anything.


  • Resource ownership (Section 7): Cluster identity tells us which cluster we're connected to, but not which resources Weaver owns. How do we handle existing Helm releases or other resources that weren't created by Weaver, especially during uninstall?

Each cluster will have a new configmap (weaver-remote-config) where we shall log who did what and when (similar to how solo-cli handles remote clusters). We shall also use labels (app.kubernetes.io/managed-by: weaver), plus ownerReferences where applicable — that’s the ownership source of truth, written with the resource itself.

Rewrite decision 4: all state is a last-known cache, there is no desired state.
Reality (cluster + host) is the source of truth and every field is re-derivable;
reconciliation has three outcomes (match, confirmed-drift, unobservable-error),
where unobservable is never treated as missing (the #1193 distinction). Add two
FAQ entries covering it.

Fold in resource ownership (section 7): ownership is authoritative on the
resources themselves via app.kubernetes.io/managed-by: weaver plus ownerReferences;
uninstall deletes only labeled resources and never touches unlabeled ones (so a
foreign Helm release is left alone). weaver-remote-config is the audit log, not
the ownership authority, and is written under the per-component lease.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
@leninmehedy
leninmehedy removed the request for review from boris-bonin October 16, 2026 12:28
@leninmehedy
leninmehedy added this pull request to stack #1299 October 16, 2026 12:29
Match the implementation (PR #1298): section 8.1 and 5.0 now describe
ScopeStartupFilesystem (pre-gate, state only), ScopeStartupHost (post-gate,
host-system, host-verified/offline) and ScopeStartupCluster (post-gate, cluster
API, provisioned + api need). Add the pre-run ordering, the version-record-after-
full-set rule, and that self-install runs filesystem + host migrations only.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>
Add a living story breakdown derived from the delivery plan (section 11) and
the spike: five milestones with dependencies, macOS-vs-VM testability, MVP
flags, the critical path (per-component state work gates the state layout), and
what can proceed in parallel now.

Signed-off-by: Lenin Mehedy <lenin.mehedy@hashgraph.com>

@alex-au alex-au left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Lenin for clearing up all of my feedbacks.

One small concern about 4th item - resource ownership: Helm already uses app.kubernetes.io/managed-by: Helm to track its resources. Not sure setting it to weaver would interfere with Helm? Perhaps we can consider using a separate weaver specific label and keep Helm's own ownership metadata intact.

@leninmehedy
leninmehedy marked this pull request as draft October 20, 2026 06:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants