M9 reflects what shipped: embedded UI bundle, signed multi-platform release pipeline + gh-pages download site, bm update self-update, and the install.sh/install.ps1 per-user installers (Homebrew/Scoop dropped). Refresh the Progress and resuming notes (mock layer removed; TS refs ported to Go).
34 KiB
Buckit Manager (bm) — Phase 1 Implementation Plan
Context
The Buckit ecosystem currently has no operator-friendly cluster
manager. Per README.md, bm is being introduced as
the operational control plane for Buckit deployments — a single
binary embedding a CLI, an HTTP API, and a web UI, with agentless
SSH orchestration over a shared application core. Phase 1
(phase1-web-ui.md) scopes the first release to
two operator wizards driven by the web UI:
- Deploy a new Buckit cluster to fresh hosts over SSH.
- Migrate an existing MinIO deployment to Buckit via in-place binary swap on the same disks.
Phase 1 builds the foundation (bm web + embedded web UI +
task engine + SSH layer + state store). The richer CLI (Phase 2),
mc admin replacement (Phase 3), and broader object-client surface
(Phase 4) all reuse the same shared core and are explicitly out of
scope for this plan.
The work lives in a new, self-contained Go module at the repo-level
bm/ directory (peer to buckit/).
Stack (locked)
| Layer | Choice | Rationale |
|---|---|---|
| Language | Go (1.25, matching buckit/) |
Single static binary, SSH client maturity, clean cross-compile |
| HTTP router | github.com/go-chi/chi/v5 |
Idiomatic net/http, mature middleware, minimal deps |
| Storage | go.etcd.io/bbolt with short-lived locks + flat task log files at tasks/<id>.log |
Pure Go (~200 KB), transactional, CLI can read RO while server holds RW only during writes |
| Event stream | Server-Sent Events (text/event-stream) |
One-way fits task logs; trivial in Go; proxy-friendly |
| Frontend | React 18 + Vite + TypeScript | Wizard density + table/topology UI; ecosystem includes @tanstack/react-table, @tanstack/react-query, react-router |
| Embedding | Go embed.FS of web/dist into the binary |
Single artifact for release |
| SSH | golang.org/x/crypto/ssh + github.com/pkg/sftp |
Standard, no shellouts to ssh/scp |
| Logging | log/slog (stdlib) |
Structured logs, zero extra deps |
| Target binary size | ~10–12 MB | bbolt + small UI bundle |
| Target platforms | linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64 (server mode primarily Linux) | Pure Go for clean cross-compile |
Storage model
A store.Store interface in front of bbolt with the open-per-transaction
pattern:
type Store interface {
View(fn func(Tx) error) error // RO open, multi-process safe
Update(fn func(Tx) error) error // RW open, server-only writer
Close() error
}
Each call internally does bbolt.Open(...) → txn → Close(), with a
5s Timeout on Open() to avoid hangs under contention. The server
holds no lock when idle, so a bm CLI running read-only commands is
always safe alongside bm web. Phase 2+ CLI write commands route
through the HTTP API (per README "manager-backed operations") and
never open the bbolt file directly.
Bbolt buckets:
clusters— cluster records (id → JSON-encodedCluster).nodes— node records keyed by<clusterID>/<nodeID>.node_facts— discovery results keyed by<clusterID>/<nodeID>.specs— topology/plan specs keyed by cluster id.tasks— task metadata (id →Task).task_index— secondary index(state, started_at) → taskID.audit— append-only audit events keyed by ULID.credentials— SSH + admin credentials, AES-GCM encrypted with the data key (see below).meta— schema version, manager UUID, root admin hash.
Data key bootstrap. The AES-GCM key for the credentials bucket
resolves in this order on startup:
BM_DATA_KEYenv (base64-encoded 32 bytes) — for operators who want the key out of the data dir entirely (mount from a secret manager, KMS, etc.).~/.config/bm/data.key(%APPDATA%\bm\data.keyon Windows) — a 32-byte file, mode0600.- Neither exists → generate a fresh 32-byte key from
crypto/rand, write it to path #2 with mode0600, and log one line on startup noting the path so the operator knows to back it up alongsidebm.db.
bm refuses to start if the file exists but has the wrong size or
world/group-readable permissions — fail loudly rather than silently
downgrading. Personal-tool framing: zero-friction default via
auto-gen, escape hatch via env for operators who care about
backup-leakage scenarios.
Task logs never go into bbolt. They stream to
~/.config/bm/tasks/<task-id>.log (rotated by size, capped per-cluster).
The task record stores a path + offset + size. SSE streams tail the
file directly.
Repository layout (bm/)
bm/
├── go.mod module github.com/buckit-io/bm
├── go.sum
├── Makefile build, test, lint, web, package, release
├── README.md
├── cmd/bm/
│ └── main.go entry point; dispatches to subcommands
├── internal/
│ ├── app/ shared application services (the "core")
│ │ ├── app.go App struct: wires store, tasks, ssh, deploy
│ │ ├── clusters.go cluster CRUD + draft state machine
│ │ ├── deploy.go new-cluster deploy workflow
│ │ ├── migrate.go MinIO migration workflow
│ │ ├── discovery.go node discovery
│ │ ├── preflight.go preflight checks
│ │ ├── topology.go topology planner & validator
│ │ └── health.go health probing
│ ├── api/ HTTP layer
│ │ ├── server.go chi router + middleware chain
│ │ ├── auth.go local admin login, session cookies
│ │ ├── clusters.go /api/v1/clusters CRUD handlers
│ │ ├── wizards.go discover/preflight/deploy/migration endpoints
│ │ ├── tasks.go /api/v1/tasks + /events SSE
│ │ ├── settings.go /api/v1/settings
│ │ └── ui.go serves embedded React bundle
│ ├── store/ bbolt persistence
│ │ ├── store.go Store interface + bbolt impl
│ │ ├── clusters.go typed accessors for cluster bucket
│ │ ├── nodes.go
│ │ ├── tasks.go
│ │ ├── audit.go
│ │ └── credentials.go AES-GCM at rest
│ ├── tasks/ task engine
│ │ ├── engine.go in-process runner, worker pool
│ │ ├── task.go Task model + state machine
│ │ ├── log.go log writer (file) + tailer (for SSE)
│ │ └── events.go pub/sub for live task events
│ ├── ssh/ SSH execution
│ │ ├── client.go pooled clients, keyed by host+creds
│ │ ├── exec.go Run, RunStream, sudo wrapping
│ │ └── sftp.go file upload (binaries, env files)
│ ├── deploy/ deploy workflow primitives
│ │ ├── package.go fetch buckit-*.rpm/.deb from GitHub Releases, cache
│ │ ├── install.go per-node: scp + dnf/apt/apk install
│ │ ├── envfile.go render /etc/default/minio
│ │ ├── systemd.go daemon-reload, enable/start, status
│ │ └── rollback.go used by migrate; reverses cutover
│ ├── cluster/ domain models
│ │ ├── cluster.go Cluster, Pool, NodeRef, Status
│ │ ├── topology.go topology compute (set size, parity, usable)
│ │ └── minio.go MinIO admin probe (snapshot, validate)
│ ├── auth/ local admin: hash, sessions, CSRF
│ │ └── auth.go
│ ├── config/ server config (listener, TLS, paths)
│ │ └── config.go
│ └── version/ build-time version metadata
│ └── version.go
├── web/
│ ├── package.json vite, react, @tanstack/*, react-router
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html
│ └── src/
│ ├── main.tsx
│ ├── routes.tsx react-router config
│ ├── api/ typed fetch wrappers + react-query hooks
│ ├── components/ see Component Inventory below
│ ├── pages/
│ │ ├── Login.tsx
│ │ ├── Welcome.tsx
│ │ ├── Clusters.tsx
│ │ ├── ClusterDetail.tsx
│ │ ├── NodeDetail.tsx
│ │ ├── Tasks.tsx
│ │ ├── TaskDetail.tsx
│ │ ├── Settings.tsx
│ │ └── wizards/
│ │ ├── NewCluster/ 8 step components
│ │ └── MigrateMinio/ 9 step components
│ └── styles/
├── web/embed.go //go:embed dist/* → fs.FS for api/ui.go
└── packaging/
├── nfpm.yaml bm rpm/deb (mirrors buckit's pattern)
├── bm.service systemd unit for bm server
├── install.sh thin POSIX installer
└── install.ps1 Windows installer
Phase 1 deliverable scope
In scope:
bm webrunning on a single host, with the embedded web UI.- Local admin auth (single user, bcrypt password, cookie session).
- Cluster create + draft persistence.
- Add nodes + SSH credentials, reachability probe.
- Discovery: OS/CPU/RAM/disks/NICs/clock skew/existing services.
- Topology planner: pool, set size, parity (EC:2/3/4/6/8), capacity math.
- Preflight (new + migrate variants).
- Deploy: fetch package from GitHub Release, scp, install via dnf/apt/apk, write
/etc/default/minio,systemctl enable --now buckit, health-wait. - MinIO migration: snapshot via admin API, in-place sequential cutover, verify, rollback, finalize.
- Tasks Center: list, detail, live log via SSE, cancel, pause.
- Cluster detail: overview, nodes tab, node detail, services tab, settings tab.
- Manager settings: change admin password, view TLS cert metadata.
- Packaging: nfpm rpm/deb,
install.sh,install.ps1.
Explicitly out of scope (deferred):
- Phase 2 CLI write commands — only
bm web,bm version,bm migrate-db, and read-onlybm cluster ls/bm tasks lsship in Phase 1. - Concurrent ("two at a time") migration rolling — sequential only.
- Optional remote-access mode (passcode + TLS) — design noted in
ui-architecture.md, but the listener stays localhost-only in Phase 1. - Multi-user / RBAC —
bmis a personal tool; not on the roadmap. - Kubernetes.
mcadmin replacement.- Bucket browser, IAM editor, metrics (delegated to per-cluster Buckit console via "Open Buckit console" link).
Implementation milestones
Each milestone is a vertically sliced, demoable chunk. Build in this order so the UI always has working endpoints to call.
M0 — Module bootstrap
go mod init github.com/buckit-io/bm, Makefile (build,test,lint,web,release),.golangci.yml, baselinecmd/bm/main.goprintingbm version.web/Vite+React+TS scaffold; dev server proxies/api/*tolocalhost:9443.- CI placeholder matrix (linux/darwin/windows × amd64/arm64).
M1 — Storage + server shell
internal/storebbolt impl with the bucket layout above and short-lived-lock pattern. Data dir at~/.config/bm/(%APPDATA%\bm\on Windows).internal/apichi router, middleware (logging, recover),/api/v1/healthz. Bind defaults to127.0.0.1:9443.internal/configreads optionalbm.yaml+ env overrides.bm websubcommand: starts the listener, opens the default browser unless--no-browser, runs in foreground; Ctrl-C exits with a confirm prompt if any task is in flight.- No auth in default localhost mode. Optional remote-access mode (passcode + TLS, opt-in via Settings) deferred to a later milestone.
M2 — Task engine + SSE
internal/tasks: in-process worker pool,Taskmodel with state machine (pending → running → succeeded|failed|canceled), step substructure, pause/cancel signals.- Log writer streams to
tasks/<id>.log; in-memory ring buffer feeds late subscribers without re-reading the file. /api/v1/tasks(list/get) +/api/v1/tasks/:id/events(SSE).- Frontend: Tasks page + Task Detail page +
TaskLogStreamcomponent wired against SSE.
M3 — SSH layer + node CRUD
internal/ssh: client pool, key/agent/password auth, sudo wrapping,Run,RunStream,Upload.internal/apicluster endpoints (create draft, patch, add nodes).NodeTable+SSHCredentialsFormUI components.- N2/M2 wizard steps: add hosts, parse paste-list, SSH reachability probe.
M4 — Discovery
internal/app/discovery: parallel SSH fact collection (OS, kernel, CPU, RAM, NICs, disks vialsblk -J, time viadate +%s, existing services viasystemctl is-active minio buckit).- Discovery runs as a task; results write to
node_facts. - N3/M3 wizard steps: live per-row progress, expandable detail panel.
M5 — Topology + preflight
internal/cluster/topologycompute and validation (uniform drives/node, set size divides total, capacity & tolerance).internal/app/preflightchecks; new-cluster and migration variants.- N4, N5, M6 wizard steps;
TopologyBuilder,PreflightTablecomponents.
M6 — New-cluster deploy
internal/deploy/package: GitHub Release fetcher with cache at/var/lib/bm/cache/, sha256 verify.internal/deploy/install: detect package manager (dnf/apt/apk), scp + install, fallback to raw binary + manager-written unit.internal/deploy/envfile: render/etc/default/miniowithMINIO_VOLUMES,MINIO_OPTS, generatedMINIO_ROOT_USER/MINIO_ROOT_PASSWORD.internal/deploy/systemd: daemon-reload, enable --now, health probehttp://node:9000/minio/health/live.- N6 Review, N7 Deploy (task-backed live view), N8 Done with one-time credential reveal.
M7 — Cluster operations
- Health probe job (per-cluster, default 30s).
- Cluster detail Overview, Nodes, Node Detail, Services tab (rolling restart reuses task engine).
- "Open Buckit console" deep-link.
- Cluster Settings tab; root credential rotation task.
M8 — MinIO migration
internal/cluster/minio: MinIO admin API probe — gather snapshot of buckets, IAM, bucket configs, replication targets.- M1–M7 wizard steps.
- Cutover task (sequential): stop minio → scp+install buckit pkg → disable minio → enable+start buckit → node-healthy probe → cluster-healthy gate → next node.
- Verify task: re-snapshot + diff against pre-snapshot + smoke PUT/GET/DELETE.
- Rollback task: reverse order, re-enable
minio.service. - Finalize task:
dnf/apt remove minio, snapshot archived, status → Active.
M9 — Packaging + installers + embed
- Embed
web/distinto the binary via//go:embed all:web/dist(lives at repo-rootuiassets.go, packagebmassets, not the originally-sketchedweb/embed.go). - Release pipeline (
.github/workflows/release.yml, triggered onRELEASE.*tags) cross-compiles all 5 target platforms, emits per-binary.sha256sum+ achecksums.txtmanifest, and signs each artifact with minisign. Publishes a GitHub Release plus a gh-pages download site (buckit-io.github.io/bm) with self-update pointer files atmanager/bm/release/<platform>/bm.sha256sum. - In-app self-update:
bm update [--check](internal/update) reads the gh-pages pointer and applies the new binary, verifying SHA-256 + minisign signature against the embedded Buckit public key. - Per-user install:
packaging/install.sh(curl|sh) andpackaging/install.ps1(PowerShellirm|iex), published to gh-pages asinstall.sh/install.ps1. They detect OS/arch, download the matching signed binary, hard-gate on SHA-256 (best-effort minisign), and install to~/.local/bin(%LOCALAPPDATA%\Programs\bmon Windows) with no sudo. macOS quarantine xattr is stripped client-side — binaries are intentionally not codesigned/notarized. No system-wide install, no systemd unit, nobmsystem user —bmis a personal-tool binary. Homebrew tap and Scoop manifest were considered and dropped in favour of the two install scripts +bm update.
Progress
Canonical state for resuming work in a new session. Last updated
2026-05-31. The git repo (git@github.com:buckit-io/bm.git,
branch main) holds everything checked off below.
✅ Completed
Documentation (in buckit/docs/manager/)
phase1-implementation.md— this docui-architecture.md— full spec for the data flow, fetch pipeline, REST contract sketch, theCluster/Node/Task/HistoryEntry/HealthSummarytypes, theExtending Buckit(HostInfo) proposal, and per-page details for Clusters list, Cluster detail, History, and Manager Settings
M0 — Module bootstrap
- Go module at
github.com/buckit-io/bm, Makefile,.golangci.yml, baselinecmd/bm/main.go bm versionandbm helpland;bm web(and thebm serveralias) are stubs printing "not yet implemented (M1)"web/Vite + React + TypeScript scaffold with@tanstack/react-query,@tanstack/react-table,react-router-dom- CI workflow placeholder: matrix builds for linux/darwin/windows × amd64/arm64, plus web typecheck + build
Web UI prototype (clickable end-to-end on top of an in-memory
mock layer at web/src/mock/{data.ts,api.ts} + web/src/api/hooks.ts)
- UI-P0 — Routing, layouts (
AppShell,WizardShell), design tokens (light + dark viaprefers-color-scheme),Pill,Stepper,TaskStateIconprimitives - UI-P1 — Welcome, Clusters list (with Refresh button +
Fetched Ns agostaleness display, multi-pool support, severity- coloured numerator on Nodes/Drives ratios), Manager Settings (rebuilt as Preferences / Storage / Remote access / About per the personal-tool framing — auth + TLS + audit retention sections intentionally dropped). Login route exists but isn't in the default flow; lands when remote-access mode flips on - UI-P2 — Tasks Center, Task Detail,
TaskLogStream,TaskStepsTimelinewith synthetic SSE-shaped log ticking - UI-P3 — Cluster detail. Major redesign mid-prototype:
collapsed from a tabbed layout to a single monitoring + ops page
centred on the node table. Per-pool health card with severity-
ordered truncation (worst pools always visible; expand to see all),
cluster Actions menu (Rolling restart / Rolling upgrade / Stop all /
Start all / Rotate root creds / Tear down). Node table has 4 probe
columns (Ping / SSH / S3 API / Console), per-column filters with
text inputs for Host + Kernel and dropdowns for Pool / State /
Version, sortable headers (default Pool asc → hostname asc within
pool, with stable tiebreaker on every other sort key), multi-select
- always-on bulk action bar (Restart buckit.service / Redeploy / Reboot host / Shut down host) disabled when SSH not configured or selection empty. Node Detail page has System / Hardware / Service / Connectivity cards
- UI-P4 — New Cluster wizard (8 steps: Basics, Add Nodes, Discovery, Topology, Preflight, Review, Deploy, Done) with animated mock progress
- UI-P5 — MinIO Migration wizard (9 steps: Basics, Add Nodes, Discovery, Snapshot, Plan, Preflight, Cutover, Verify, Finalize) with typed-confirm Finalize modal
- History tab — added beyond original P-tasks scope. Records
literal CLI commands (future) and UI action descriptions (current);
filter chips + target dropdown + search + Copy on CLI rows + jump-
to-task on rows with a
taskId. Wired up so cluster Actions menu- per-host bulk actions + Services-style buttons all append rows
- Mock data computes derived state —
computeHealthSummary+computeHealth+ per-pool rollup live inweb/src/mock/data.tsand are the reference implementation that ports to Go in M1+ - Cross-cutting fixes — every
padding: 0, overflow: "hidden"table card switched to a shared.card--tableutility class withoverflow-x: autoso wide tables scroll horizontally instead of clipping at narrow viewports
Backend implementation milestones M1–M9 — all landed. The real Go
backend (bbolt store, chi API, task engine + SSE, SSH layer, discovery,
topology + preflight, new-cluster deploy, cluster operations, MinIO
migration) and the React UI fetching against the live HTTP surface are
in main. M9 specifically: the UI bundle is embedded
(//go:embed all:web/dist in uiassets.go), a signed multi-platform
release pipeline publishes to GitHub Releases + a gh-pages download
site, bm update self-updates with SHA-256 + minisign verification,
and packaging/install.sh + packaging/install.ps1 provide the
per-user one-liner installers (see the M9 milestone above for detail).
The cross-cutting TODOs listed below (cutover drop-in, rollback action,
preflight checks, alias auto-save, follow-up wizards) remain open and
are tracked individually.
⬜ Not yet started
Web UI prototype
- UI-P6 — Cross-cutting polish: skeleton loaders, lost-connection banner, full keyboard nav through wizards, a11y pass on status pills/icons. Deferred to the end of the prototype phase
- UI-P7 — Derive backend API contract from the prototype. Largely
already captured in
ui-architecture.md(REST contract sketch, type shapes, per-page details for Clusters list, Cluster detail, History, Manager Settings). Per-page docs for the wizards (/clusters/new,/clusters/migrate) and for Tasks Center + Task Detail are stubbed in the doc and need to be filled in - TODO — Add new pool wizard. The
Actions ▾ → Add new pool…menu item is wired up on/clusters/:id(ClusterDetail.tsxCLUSTER_ACTIONS) but currently only records a History row. A proper wizard is still to be built. Sketch: reuses the New Cluster wizard's Add Nodes → Discovery → Topology → Preflight steps, then a Cutover step that rolling-restarts every node with an expandedMINIO_VOLUMESenvfile. Constraints worth calling out in the doc once it's drafted: the new pool's per-set drive layout must match existing pools (uniform set size), parity is fixed cluster-wide and not re-selectable, and the rolling restart is mandatory becauseMINIO_VOLUMESis env-only (seerequest-flow.md§ 10.1). Backend side belongs in a post-Phase-1 milestone — not part of M1–M9 today. - TODO — M8 cutover: write systemd drop-in for buckit User/Group.
Buckit's rpm ships
buckit.servicewithUser=buckit/Group=buckit(verified inbuckit/packaging/buckit.service+postinstall.sh— postinstall does NOT auto-start the service, so install-first cutover is safe). But the source minio is typically running asminio:minio(or similar) and owns the data drives. If buckit starts asbuckit:buckitit can't read.minio.sys/or the shard files.- Cutover must detect minio's runtime user/group:
systemctl show minio.service -p User -p Group(parse theUser=…/Group=…lines). Falls back toroot:rootif minio runs as root. - Write
/etc/systemd/system/buckit.service.d/override.confper node:Mode[Service] User=<source-minio-user> Group=<source-minio-group>0644, ownerroot:root. Atomic write (tmp + rename). - Drop-in goes under
/etc/systemd/system/…d/(not/lib/…) so it survives package upgrades. systemctl daemon-reloadafter writing the drop-in, before starting buckit.- Rollback path: drop-in stays on disk after rollback. Harmless because buckit.service isn't running. Re-running the migrate wizard later replays the same logic.
- Preflight implication: capture the source minio user/group during
Discover, surface in the Plan step ("buckit will run as
<user>:<group>to match minio") so the operator sees what's happening before they ack. - Why drop-in vs
chown -R buckit:buckit /data/disk*: chown on a 16 TiB drive with millions of files takes minutes. Drop-in is instant and symmetric for rollback.
- Cutover must detect minio's runtime user/group:
- TODO — Rollback-to-MinIO cluster action. The migrate wizard no
longer has a Finalize step (Phase 1 prototype dropped it). The
minio package stays installed on every node post-cutover so the
operator can roll back at any time. Backend behavior to implement
alongside M8:
- Cluster detail page Actions menu gains "Rollback to MinIO"
item, visible when
cluster.migratedFromis set. - Symmetric to cutover: for each node in sequence — stop buckit,
disable buckit.service, re-enable minio.service, wait for
node-healthy probe (minio's
:9000/minio/health/live). - On success: flip
cluster.engineback to"minio", keep both packages installed (operator can re-run the migrate wizard later). - Typed-confirm modal: type the cluster name.
- Tracked on a separate History row.
- Cluster detail page Actions menu gains "Rollback to MinIO"
item, visible when
- TODO — M5 preflight: detect stale
.minio.sys/format.json. When a deploy reuses a drive from a prior Buckit/MinIO deployment (different deployment ID), the server refuses to start with a cryptic error. Preflight should SSH into each host andtest -f <mount>/.minio.sys/format.jsonfor every chosen mountpoint, then parse the file'sid(deployment ID). If any drive's deployment ID is set and doesn't match an empty / about-to-be-created cluster, surface a warning row: "<mount>on<host>already belongs to a different deployment. The server will refuse to start. Wipe withrm -rf <mount>/.minio.sys(destroys data) or pick a different drive." High-frequency support issue in practice — operators who retry after a failed deploy hit this. Belongs in M5 alongside the existing preflight checks; not a separate milestone. - TODO — Auto-save bm alias on successful deploy / import. The
New Cluster Done step and the Import flow's success path both
promise that a
bmalias has been saved to the operator's~/.bm/config.json(the prototype's UI copy says so). This isbm's own alias file — separate frommc's~/.mc/config.json— consumed by futurebm alias/bm admin …CLI subcommands. Backend behaviorbmshould implement when M6 lands:- Resolve config dir:
BM_CONFIG_DIRenv, else~/.bm/(%USERPROFILE%\.bm\on Windows). Create the dir mode0700if missing. (Distinct from the bbolt data dir at~/.config/bm/; the alias file is operator-facing, the bbolt is manager-internal.) - Read existing
config.jsonor initialize the skeleton if absent. Schema mirrors mc's v10 shape for familiarity:{ "version": "1", "aliases": { "<alias>": { url, accessKey, secretKey, api: "S3v4", path: "auto" } } }. - Alias key is derived from the cluster name — slugified to
[a-z0-9-]{1,32}(lowercase, non-alphanumeric →-, collapse repeats, trim ends). Cluster display name stays free-form;My Production East!becomes aliasmy-production-east. Implemented at the Done step (toAliasNameinpages/wizards/new/steps/Done.tsx) — port the same rule when M6 writes config.json. - Atomic write: write
config.json.tmpthen rename. Mode0600. - If an alias with the same name already exists pointing at a different URL, prompt the operator before overwriting (likely an unrelated collision — they probably don't want it clobbered).
- The Done step's "Run on another machine" disclosure stays as the
fallback for operators whose
bmlives on a different host.
- Resolve config dir:
- TODO — Drive preparation wizard (post-Phase-1). The Topology
step today bails to "mount drives consistently and re-run
discovery" when no common mountpoints exist (case C). A proper
flow would let bm format and mount raw drives during deploy with
passwordless sudo. Out of scope for Phase 1; in scope for a
follow-up milestone (call it M6.5). Safety requirements before this
ships:
- Refuse to prep any drive with an existing filesystem signature
unless the operator explicitly types the device path to wipe.
wipefson the wrong drive is unrecoverable. - Require passwordless sudo. If absent, the wizard is unavailable.
- Show every command bm will run, per host, before the operator confirms (matches the History tab's literal-CLI treatment).
- Typed-confirm modal (cluster name or
PREPARE DRIVES) before any destructive command runs. - Anchor
/etc/fstabentries byUUID=not device name, so reboot- drive-letter shuffles don't break the deployment.
- Default mountpoint pattern is
/data/disk{1..N}. Default fs is XFS (MinIO recommendation). Operator can override both. - Only a single path in the codebase ever touches a raw block
device. No other M6/M7/M8 operations call
wipefs/mkfs.
- Refuse to prep any drive with an existing filesystem signature
unless the operator explicitly types the device path to wipe.
Notes for resuming
- M1–M9 are landed. Remaining work is the cross-cutting TODOs above (cutover systemd drop-in, rollback-to-MinIO action, the two M5 preflight checks, alias auto-save, and the follow-up wizards) plus the optional remote-access mode (passcode + TLS), which stays deferred — the listener is localhost-only today.
- The mock layer is gone.
web/src/mock/was removed once the real backend landed; every UI fetch now funnels throughweb/src/api/client.ts(SSE viaweb/src/api/sse.ts). The live REST surface — not a mock — is now the contract. Wire types stay in lockstep betweeninternal/domain/(Go) andweb/src/api/types.ts. - The TS reference implementations were ported to Go.
computeHealthSummary,computeHealth,summarizePools, andcompareNodes(sort comparator with stable tiebreaker) now live in the Go backend; the node-table default sort (pool asc, hostname asc within pool) and pool-card severity ordering are the canonical behaviours to preserve. - The prototype does not yet use the proposed
madmin-goHostInfofork — fields likecpuModel,kernelare populated directly in the mock fixture. When the real backend lands, the merge step ininternal/app/refresh.goreads these from/minio/admin/v3/info'sHostsubstruct (seeui-architecture.md§ "Extending Buckit to return host info" for the proposal). - Doc anchors to keep in sync when behaviour changes: the
"Per-page details" sections in
ui-architecture.mdand this doc's Progress section.
Nothing is imported from the buckit/ module — bm is a separate
module that talks to buckit over SSH and HTTP, never as a library.
What we do consume from buckit:
- Release artifacts produced by
buckit/packaging/nfpm.yamlandbuckit/packaging/buckit.service—bmfetches these from the GitHub Release for the target version and installs them on nodes. The unit file'sEnvironmentFile=-/etc/default/miniocontract is whatbmwrites against. - Health endpoint
http://node:9000/minio/health/livefor the post-install probe. - MinIO admin API surface (used during migration snapshot/verify).
We deliberately do not import any buckit/internal/* package —
that would couple the manager to the object server's Go API and
break the "manager around / console inside" separation in the spec.
Verification
End-to-end test plan once milestones are landed:
-
Build + binary size check
cd bm && make build ls -lh bm # confirm 10-12 MB target ./bm version -
Local smoke (no real cluster needed)
BM_INITIAL_ADMIN_PASSWORD=admin ./bm server --listen :9443Browse
https://localhost:9443, log in, see Welcome screen. Frontend dev mode:cd web && npm run dev; verify Vite proxy works. -
Unit tests —
make testcovers store, topology, envfile rendering, MinIO snapshot diff, task state machine. Integration tests can usetestcontainers-goto spin a realbuckitcontainer for health-probe and unit-file contract tests. -
New-cluster wizard against local VMs — 3-VM Vagrant or multipass lab (Ubuntu 24.04, RHEL 9, Alpine — exercises all three package managers). Run the new-cluster wizard end-to-end via the UI. Verify
/etc/default/minioexists,buckit.serviceis active,mc admin infoagainstnode1:9000succeeds. -
MinIO migration against a real MinIO lab — 4-node MinIO setup with 12 buckets, IAM users, lifecycle rules. Run migration wizard; confirm verify reports parity; finalize. Confirm
dnf list installed minioreturns empty post-finalize. Test rollback on a parallel 4-node cluster: cutover 2 nodes, click Rollback, confirmminio.serviceis back up. -
CLI-vs-server coexistence — with
bm webrunning and an active deploy task, runbm cluster lsandbm tasks lsfrom another shell. Confirm both return promptly (< 100 ms) and show consistent state — proves the bbolt short-lived-lock model works under load. -
Cross-compile sanity — from a single host:
GOOS=linux GOARCH=arm64 go build ./cmd/bm GOOS=darwin GOARCH=arm64 go build ./cmd/bm GOOS=windows GOARCH=amd64 go build ./cmd/bmAll should succeed without CGO.
Deferred decisions (from README "Open Questions")
These do not block Phase 1 but should be revisited before Phase 2:
- SSH credential rotation policy — Phase 1 stores SSH creds AES-GCM at rest using
BM_DATA_KEY. Rotation UX lives in Cluster Settings → "Rotate" and re-encrypts in place. KMS-backed key is Phase 3. - Multi-user / RBAC — single local admin in Phase 1. Auth layer is structured (
auth.User,auth.Session) so adding user/role tables later is incremental. - Postgres backend —
store.Storeinterface keeps this open; not implemented in Phase 1. - "Two-at-a-time" migration concurrency — sequential only in Phase 1, per the open question in
phase1-web-ui.md.