fleet-deploy: - Rename harmony-fleet-release binary to harmony-fleet-publish - Route all deploy settings through ConfigClient (env → OpenBao → prompt) instead of bespoke flags; seed FleetDeploySecrets via OpenBao - Rename HARMONY_SECRET_NAMESPACE to HARMONY_CONFIG_NAMESPACE - Append -chart to the Helm chart artifact name so it no longer collides with the Docker image in Harbor (application/vnd.cncf.helm.config.v1+json) harmony_cli: - Switch from log to tracing for structured output - Defer topology prep so --list and declined runs are no-ops - Drop ANSI colour codes around log emojis - Init cli logger in fleet deploy binary openbao: - Scope unseal-keys cache file per instance - Example gains setup capability and updated README roadmap: - Add unified CLI design document (ROADMAP/13-unified-cli.md) - Update v0.3 fleet platform plan Squashed commit of the following: commit36d9d9aaecMerge:12c8d9cfe7148aa8Author: johnride <jg@nationtech.io> Date: Mon Jun 1 15:42:56 2026 +0000 Merge pull request 'fix: fleet operator chart name was conflicting with the container name. Append -chart to the chart name' (#317) from fix/fleet-operator-chart-name into chore/rename-release-to-publish Reviewed-on: #317 commite7148aa85fAuthor: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Mon Jun 1 11:35:15 2026 -0400 fix: fleet operator chart name was conflicting with the container name. Append -chart to the chart name commit12c8d9cfa0Author: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Mon Jun 1 11:12:23 2026 -0400 feat: Init cli logger in fleet deploy commitedb62668b6Author: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sun May 31 12:56:36 2026 -0400 doc: Roadmap entry for cli design and implementation commitf2ecccb4abAuthor: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sun May 31 12:32:19 2026 -0400 refactor(fleet-deploy): rename harmony-fleet-release to harmony-fleet-publish Deploy/publish wording is more intuitive than deploy/release. commit2e9052b217Author: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sun May 31 10:12:54 2026 -0400 fix(openbao): remove extra blank line in example Pre-existing formatting issue caught by cargo fmt --check. commitf7299ebe2bAuthor: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sun May 31 09:13:39 2026 -0400 refactor(fleet-deploy): rename HARMONY_SECRET_NAMESPACE to HARMONY_CONFIG_NAMESPACE The env var name was a misnomer — ConfigClient resolves both config and secrets, not just secrets. The struct field was already config_namespace. Legacy SecretManager keeps the old var; this forces migration to ConfigClient for new code. commitd39aa15152Author: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sun May 31 09:06:20 2026 -0400 feat: fleet deploy uses configuration from configclient for all settings, update the 0_3 plan commit57d056fcedAuthor: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sat May 30 11:07:03 2026 -0400 fix(openbao): scope unseal-keys cache file per instance The root token + unseal keys were written to a single fixed `~/.local/share/harmony/openbao/unseal-keys.json`, so deploying a second OpenBao instance (different namespace/release) overwrote the first's keys — after which the first could never be unsealed. Key the file by namespace+release (`unseal-keys-<ns>-<release>.json`); `cached_root_token` now takes the `OpenbaoInstance` to read the right one. commit44aa83199aAuthor: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sat May 30 11:05:30 2026 -0400 fix(harmony_cli): drop ANSI colour codes around log emojis `console::style(emoji).green()/.yellow()/.red()/.blue()` embedded raw ANSI escapes in the message string. `console` force-emits them off its own TTY detection, which disagrees with the tracing writer, so they leaked as literal `\x1b[..m` garbage around the emoji. Emit plain emojis — the glyph already conveys status and the tracing fmt layer still colours the level. commit4fef957edbAuthor: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sat May 30 08:40:54 2026 -0400 feat: Example openbao now can do openbao setup and better readme commitaf3205d353Author: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sat May 30 05:55:49 2026 -0400 refactor(harmony_cli): defer topology prep so --list/declined runs are no-ops `Maestro::initialize` (hence `topology.ensure_ready()`) ran before `init`'s `--list` / confirmation short-circuits, so merely listing a binary's scores — or declining to run them — still prepared the topology (cert-manager install, etc.). Build the maestro unprepared and call `prepare_topology()` only once we commit to interpreting. Expose `Maestro::prepare_topology`; add tests proving `--list` skips prep while the run path triggers it. commit199e285e52Author: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Sat May 30 05:04:34 2026 -0400 feat: Use tracing instead of logger in harmon_cli and work on fleet_staging_install refactor to use harmony_cli properly, still some more work to do commitfac83d853dAuthor: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Fri May 29 22:39:39 2026 -0400 refactor(fleet-staging): use tracing instead of println for output Swap env_logger for tracing_subscriber (its fmt bridges the framework's log:: deploy-progress output) and route the install banner + step logs through tracing::info! — no raw println. commit0400e9d454Author: Jean-Gabriel Gill-Couture <jg@nationtech.io> Date: Fri May 29 20:25:22 2026 -0400 feat(fleet-staging): add OpenBao + seed FleetDeploySecrets; route operator creds through the deploy crate fleet_staging_install now deploys OpenBao (co-located in fleet-staging, cert-manager TLS at secrets-stg.<base>), configures it (fleet-deployer read policy), and seeds the operator's FleetDeploySecrets so the operator can be upgraded alone via 'harmony-fleet-deploy --from-tag'. Behavior of the existing bring-up is unchanged. Credential-TOML construction moved out of the example into OperatorCredentials::zitadel_jwt (deploy crate) so all callers share it. New openbao::cached_root_token() lets the seed reuse the root token setup already cached. Seeding mirrors the harmony_sso port-forward pattern.
10 KiB
Phase 13: Unified CLI — Extensible, Composable, Subcommand-Driven
Goal
Replace the current landscape of disconnected harmony-* binaries and
60+ example main.rs files with a single, extensible CLI where:
- The framework provides global concerns (config, SSO, topology selection, Score runner, TUI) as shared infrastructure.
- Each module (fleet, tenant, okd, …) registers its own subcommands.
- Third-party
MyAppScoreauthors getharmony myapp deploywith zero framework boilerplate.
The CLI is the user-facing surface of Harmony. Every design decision here shapes the developer experience for the entire ecosystem.
Current State
harmony_cli::Args— flat Score-runner flags (--yes,--filter,--list,--number,--interactive). Drives the Maestro loop over aVec<Score>.harmony_cli::run(Inventory, Topology, Vec<Score>, Option<Args>)— the single entry point consumed by 60+ example binaries.harmony_tui::run()— separate crate, separaterun(), same inputs.harmony-fleet-deploy— deploy binary withdeploy/publishsubcommands (just merged from two separate binaries).harmony_composer— infrastructure composition tool, separate binary.- ADR-023 principle 8 describes the staged evolution (B → C) but defers the plugin protocol.
Design
Top-level binary with subcommands
harmony [global flags] <module> <action> [action flags]
harmony --config-namespace fleet-staging fleet deploy --from-tag v0.1.0
harmony --config-namespace fleet-staging fleet publish --from-tag v0.1.0 --no-push
harmony --config-namespace okd-staging okd bootstrap
harmony --config-namespace tenant-c1 tenant create --name c1
harmony --config-namespace harmony myapp deploy --image foo:latest
Global flags (owned by the top-level binary):
--config-namespace— maps toConfigClient::for_namespace()--kubeconfig— topology selection--topology— explicit topology choice (k3d, okd, bare, …)--yes— skip confirmation prompts--interactive— delegate to TUI
Module subcommands (owned by each module):
fleet deploy,fleet publishtenant create,tenant list,tenant health,tenant installokd bootstrap,okd add-node- User-defined:
myapp deploy,myapp publish, …
Two kinds of subcommands
Score-runner subcommands — compose multiple Scores, need
--filter/--list/--number. Examples, ad-hoc orchestration, the
current harmony_cli::run() use case. The Maestro loop lives here.
Action subcommands — single-purpose (deploy a chart, publish an image, create a tenant). No filter/list/number. Run one Score or a fixed composition.
The distinction matters: forcing action subcommands through the filter/list/number machinery is ceremony; forcing Score-runner subcommands into a rigid single-action shape is constraining.
Deploy crates become library-only
Per ADR-023 principle 5, deploy logic lives in *-deploy crates. The
unified CLI absorbs the binaries — deploy crates lose their
[[bin]] entries and become libraries consumed by the top-level
harmony binary. The crate boundary stays; the binary boundary goes
away.
harmony-fleet-deploy/
Cargo.toml # [lib] only, no [[bin]]
src/
lib.rs # FleetDeployConfig, FleetDeploySecrets, FleetOperatorScore
commands.rs # DeployCommand, PublishCommand (clap Subcommand structs)
The top-level harmony binary imports harmony_fleet_deploy::commands
and wires them into its own Command enum.
Publish logic as Scores
Build/push logic (currently imperative Command::new("docker") in
harmony-fleet-publish) should be encapsulated in Scores, following
the Application trait + feature composition pattern
(examples/try_rust_webapp + PackagingDeployment). The publish
subcommand becomes a thin CLI wrapper over a Score composition, not
a shell-out script.
This is not PackagingDeployment specifically — the operator isn't a
RustWebapp. The pattern is the Application trait + feature
composition model: a typed application description with composable
features (build, push, deploy, monitor).
Plugin discovery (stage C, deferred)
ADR-023 principle 8 envisions harmony discovering harmony-*
binaries on $PATH (kubectl-style). This is the third-party
extensibility story: a MyAppScore author ships a harmony-myapp
binary, and harmony myapp deploy works without rebuilding the
framework.
Open question: is the end state a monolithic binary with composable subcommands (first-party modules compiled in), or kubectl-style plugin discovery for everything? Likely both: first-party modules are compiled-in subcommands (tighter integration, shared types), third-party modules are discovered plugins (loose coupling, separate release cycles). The protocol for plugin communication (env vars, stdin JSON, exit codes) is a separate design effort.
TUI integration
harmony_tui is a separate crate with its own run(). The unified
CLI's --interactive global flag delegates to harmony_tui::run()
for Score-runner subcommands. Action subcommands may or may not have
TUI equivalents — that's per-subcommand, not global.
harmony_composer
Stays separate for now. It's an infrastructure composition tool with
a different audience (platform engineers building topologies, not
operators deploying apps). May become harmony compose later if the
use cases converge.
Tasks
13.1 Rewrite harmony_cli — subcommand-aware runner
Replace the flat Args struct with a subcommand-aware Cli struct.
Global flags move to the top level. The run() function accepts a
Command enum instead of Option<Args>.
#[derive(Parser)]
struct Cli {
#[arg(long, env = "HARMONY_CONFIG_NAMESPACE", global = true)]
config_namespace: String,
#[arg(long, global = true)]
kubeconfig: Option<String>,
#[arg(long, global = true)]
yes: bool,
#[command(subcommand)]
command: Command,
}
Files: harmony_cli/src/lib.rs, harmony_cli/src/args.rs (new)
Blocked by: Phase 02 (config migration — so the new CLI is born
on harmony_config, not retrofitted)
Blocks: 13.2, 13.3
13.2 Migrate one deploy binary to subcommand pattern
Proof of concept: harmony-fleet-deploy already has deploy/publish
subcommands. Migrate it to the new harmony_cli runner: deploy crate
becomes library-only, exports Command enum, top-level binary wires
it in.
Files: fleet/harmony-fleet-deploy/, new top-level harmony binary
Blocked by: 13.1
13.3 Migrate examples
Each of the 60+ examples currently calls harmony_cli::run() with
flat args. Migration: each example becomes a subcommand of the
top-level harmony binary, or stays as a standalone binary that
imports the new harmony_cli runner.
Migration shape (before/after):
// Before (standalone binary)
fn main() {
harmony_cli::run(Inventory::autoload(), topology, scores, None).await;
}
// After (subcommand of top-level binary)
// In the example's crate:
pub struct MyExampleCommand { /* clap args */ }
impl Subcommand for MyExampleCommand { ... }
// In the top-level binary:
enum Command {
MyExample(MyExampleCommand),
Fleet(FleetCommand),
...
}
Files: 60+ example crates Blocked by: 13.2 (prove the pattern works on one)
13.4 Publish-as-Score
Extract build/push logic from harmony-fleet-publish into Scores
following the Application trait + feature composition pattern.
The publish subcommand becomes a thin wrapper.
Files: harmony/src/modules/application/ (extend), fleet/harmony-fleet-deploy/
Blocked by: 13.2
13.5 Topology selection in the CLI
Global --topology flag or auto-detection. Requires Phase 12.6
(topology proliferation / K8sBareTopology) to land first — the
CLI's topology selection is simpler if the topology landscape is
clean.
Blocked by: Phase 12.6
13.6 Plugin discovery protocol (stage C)
Design the protocol for third-party harmony-* binaries to
communicate with the top-level harmony binary. Env vars for
global args? stdin JSON? Exit codes for outcomes?
Status: Research + ADR first. No implementation until the protocol is locked. Blocked by: 13.5 (first-party subcommands working end-to-end)
Dependencies
Phase 02 (config migration) ──→ 13.1 (CLI rewrite)
Phase 12.6 (topology cleanup) ──→ 13.5 (topology selection)
13.1 ──→ 13.2 (fleet-deploy migration)
13.2 ──→ 13.3 (example migration)
13.2 ──→ 13.4 (publish-as-Score)
13.5 ──→ 13.6 (plugin discovery)
Phase 11 (named config instances) can land after the CLI rewrite —
the global --config-namespace flag maps directly to
ConfigClient::for_namespace(), and named instances
(get_named::<T>("fw-primary")) become a CLI concern too.
ADR-023 Tensions
These need resolution during implementation:
-
Principle 5 vs. absorbing binaries. Deploy crates keep their crate boundary (library + Scores) but lose their
[[bin]]. The unified binary is the sole entry point. This is a refinement of principle 5, not a violation — the deploy logic still lives in the deploy crate. -
Principle 8 monolith vs. plugin. First-party modules are compiled-in subcommands. Third-party modules are discovered plugins. The boundary between "first-party" and "third-party" needs a clear doctrine (likely: anything in the harmony repo is first-party; everything else is a plugin).
-
harmony_composerplacement. Stays separate for now. If the use cases converge with the unified CLI, it becomesharmony compose. Not a blocker.
References
- ADR-023 principle 8 — CLI: hybrid, staged (B → C)
- ADR-023 principle 5 — deploy logic in
*-deploycrates - ADR draft 024 §Q5 — runtime tools in the dependency graph
examples/try_rust_webapp—Applicationtrait + feature compositionharmony/src/modules/application/features/packaging_deployment.rs— build/push as a Score feature- Phase 02 — config migration (prerequisite)
- Phase 11 — named config instances (parallel)
- Phase 12.6 — topology proliferation (prerequisite for 13.5)