Files
buckit/docs/manager/phase1-implementation.md
abuckit 42f49f3cc3 docs(manager): mark M1-M9 landed; document shipped installer approach
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).
2026-05-31 12:00:21 -04:00

34 KiB
Raw Permalink Blame History

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:

  1. Deploy a new Buckit cluster to fresh hosts over SSH.
  2. 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 ~1012 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-encoded Cluster).
  • 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:

  1. BM_DATA_KEY env (base64-encoded 32 bytes) — for operators who want the key out of the data dir entirely (mount from a secret manager, KMS, etc.).
  2. ~/.config/bm/data.key (%APPDATA%\bm\data.key on Windows) — a 32-byte file, mode 0600.
  3. Neither exists → generate a fresh 32-byte key from crypto/rand, write it to path #2 with mode 0600, and log one line on startup noting the path so the operator knows to back it up alongside bm.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 web running 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-only bm cluster ls / bm tasks ls ship 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 — bm is a personal tool; not on the roadmap.
  • Kubernetes.
  • mc admin 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, baseline cmd/bm/main.go printing bm version.
  • web/ Vite+React+TS scaffold; dev server proxies /api/* to localhost:9443.
  • CI placeholder matrix (linux/darwin/windows × amd64/arm64).

M1 — Storage + server shell

  • internal/store bbolt impl with the bucket layout above and short-lived-lock pattern. Data dir at ~/.config/bm/ (%APPDATA%\bm\ on Windows).
  • internal/api chi router, middleware (logging, recover), /api/v1/healthz. Bind defaults to 127.0.0.1:9443.
  • internal/config reads optional bm.yaml + env overrides.
  • bm web subcommand: 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, Task model 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 + TaskLogStream component wired against SSE.

M3 — SSH layer + node CRUD

  • internal/ssh: client pool, key/agent/password auth, sudo wrapping, Run, RunStream, Upload.
  • internal/api cluster endpoints (create draft, patch, add nodes).
  • NodeTable + SSHCredentialsForm UI 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 via lsblk -J, time via date +%s, existing services via systemctl 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/topology compute and validation (uniform drives/node, set size divides total, capacity & tolerance).
  • internal/app/preflight checks; new-cluster and migration variants.
  • N4, N5, M6 wizard steps; TopologyBuilder, PreflightTable components.

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/minio with MINIO_VOLUMES, MINIO_OPTS, generated MINIO_ROOT_USER/MINIO_ROOT_PASSWORD.
  • internal/deploy/systemd: daemon-reload, enable --now, health probe http://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.
  • M1M7 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/dist into the binary via //go:embed all:web/dist (lives at repo-root uiassets.go, package bmassets, not the originally-sketched web/embed.go).
  • Release pipeline (.github/workflows/release.yml, triggered on RELEASE.* tags) cross-compiles all 5 target platforms, emits per-binary .sha256sum + a checksums.txt manifest, 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 at manager/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) and packaging/install.ps1 (PowerShell irm|iex), published to gh-pages as install.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\bm on Windows) with no sudo. macOS quarantine xattr is stripped client-side — binaries are intentionally not codesigned/notarized. No system-wide install, no systemd unit, no bm system user — bm is 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 doc
  • ui-architecture.md — full spec for the data flow, fetch pipeline, REST contract sketch, the Cluster/Node/Task/HistoryEntry/ HealthSummary types, the Extending 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, baseline cmd/bm/main.go
  • bm version and bm help land; bm web (and the bm server alias) 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 via prefers-color-scheme), Pill, Stepper, TaskStateIcon primitives
  • UI-P1 — Welcome, Clusters list (with Refresh button + Fetched Ns ago staleness 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, TaskStepsTimeline with 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 statecomputeHealthSummary + computeHealth + per-pool rollup live in web/src/mock/data.ts and 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--table utility class with overflow-x: auto so wide tables scroll horizontally instead of clipping at narrow viewports

Backend implementation milestones M1M9 — 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.tsx CLUSTER_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 expanded MINIO_VOLUMES envfile. 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 because MINIO_VOLUMES is env-only (see request-flow.md § 10.1). Backend side belongs in a post-Phase-1 milestone — not part of M1M9 today.
  • TODO — M8 cutover: write systemd drop-in for buckit User/Group. Buckit's rpm ships buckit.service with User=buckit / Group=buckit (verified in buckit/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 as minio:minio (or similar) and owns the data drives. If buckit starts as buckit:buckit it 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 the User=… / Group=… lines). Falls back to root:root if minio runs as root.
    • Write /etc/systemd/system/buckit.service.d/override.conf per node:
      [Service]
      User=<source-minio-user>
      Group=<source-minio-group>
      
      Mode 0644, owner root:root. Atomic write (tmp + rename).
    • Drop-in goes under /etc/systemd/system/…d/ (not /lib/…) so it survives package upgrades.
    • systemctl daemon-reload after 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.
  • 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.migratedFrom is 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.engine back 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.
  • 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 and test -f <mount>/.minio.sys/format.json for every chosen mountpoint, then parse the file's id (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 with rm -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 bm alias has been saved to the operator's ~/.bm/config.json (the prototype's UI copy says so). This is bm's own alias file — separate from mc's ~/.mc/config.json — consumed by future bm alias/bm admin … CLI subcommands. Backend behavior bm should implement when M6 lands:
    • Resolve config dir: BM_CONFIG_DIR env, else ~/.bm/ (%USERPROFILE%\.bm\ on Windows). Create the dir mode 0700 if missing. (Distinct from the bbolt data dir at ~/.config/bm/; the alias file is operator-facing, the bbolt is manager-internal.)
    • Read existing config.json or 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 alias my-production-east. Implemented at the Done step (toAliasName in pages/wizards/new/steps/Done.tsx) — port the same rule when M6 writes config.json.
    • Atomic write: write config.json.tmp then rename. Mode 0600.
    • 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 bm lives on a different host.
  • 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. wipefs on 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/fstab entries by UUID= 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.

Notes for resuming

  • M1M9 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 through web/src/api/client.ts (SSE via web/src/api/sse.ts). The live REST surface — not a mock — is now the contract. Wire types stay in lockstep between internal/domain/ (Go) and web/src/api/types.ts.
  • The TS reference implementations were ported to Go. computeHealthSummary, computeHealth, summarizePools, and compareNodes (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-go HostInfo fork — fields like cpuModel, kernel are populated directly in the mock fixture. When the real backend lands, the merge step in internal/app/refresh.go reads these from /minio/admin/v3/info's Host substruct (see ui-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.md and 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.yaml and buckit/packaging/buckit.servicebm fetches these from the GitHub Release for the target version and installs them on nodes. The unit file's EnvironmentFile=-/etc/default/minio contract is what bm writes against.
  • Health endpoint http://node:9000/minio/health/live for 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:

  1. Build + binary size check

    cd bm && make build
    ls -lh bm                # confirm 10-12 MB target
    ./bm version
    
  2. Local smoke (no real cluster needed)

    BM_INITIAL_ADMIN_PASSWORD=admin ./bm server --listen :9443
    

    Browse https://localhost:9443, log in, see Welcome screen. Frontend dev mode: cd web && npm run dev; verify Vite proxy works.

  3. Unit testsmake test covers store, topology, envfile rendering, MinIO snapshot diff, task state machine. Integration tests can use testcontainers-go to spin a real buckit container for health-probe and unit-file contract tests.

  4. 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/minio exists, buckit.service is active, mc admin info against node1:9000 succeeds.

  5. 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 minio returns empty post-finalize. Test rollback on a parallel 4-node cluster: cutover 2 nodes, click Rollback, confirm minio.service is back up.

  6. CLI-vs-server coexistence — with bm web running and an active deploy task, run bm cluster ls and bm tasks ls from another shell. Confirm both return promptly (< 100 ms) and show consistent state — proves the bbolt short-lived-lock model works under load.

  7. 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/bm
    

    All 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 backendstore.Store interface 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.