Files
harmony/ROADMAP/13-unified-cli.md
Jean-Gabriel Gill-Couture 89e5e104dc
All checks were successful
Run Check Script / check (push) Successful in 2m14s
Compile and package harmony_composer / package_harmony_composer (push) Successful in 8m22s
harmony-fleet-operator — release / release (push) Successful in 3m17s
feat(fleet): unify deploy config, switch CLI to tracing, fix OCI chart name collision
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:

commit 36d9d9aaec
Merge: 12c8d9cf e7148aa8
Author: 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

commit e7148aa85f
Author: 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

commit 12c8d9cfa0
Author: Jean-Gabriel Gill-Couture <jg@nationtech.io>
Date:   Mon Jun 1 11:12:23 2026 -0400

    feat: Init cli logger in fleet deploy

commit edb62668b6
Author: Jean-Gabriel Gill-Couture <jg@nationtech.io>
Date:   Sun May 31 12:56:36 2026 -0400

    doc: Roadmap entry for cli design and implementation

commit f2ecccb4ab
Author: 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.

commit 2e9052b217
Author: 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.

commit f7299ebe2b
Author: 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.

commit d39aa15152
Author: 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

commit 57d056fced
Author: 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.

commit 44aa83199a
Author: 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.

commit 4fef957edb
Author: 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

commit af3205d353
Author: 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.

commit 199e285e52
Author: 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

commit fac83d853d
Author: 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.

commit 0400e9d454
Author: 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.
2026-06-01 11:51:11 -04:00

282 lines
10 KiB
Markdown

# 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 `MyAppScore` authors get `harmony myapp deploy` with 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
a `Vec<Score>`.
- `harmony_cli::run(Inventory, Topology, Vec<Score>, Option<Args>)`
the single entry point consumed by 60+ example binaries.
- `harmony_tui::run()` — separate crate, separate `run()`, same inputs.
- `harmony-fleet-deploy` — deploy binary with `deploy`/`publish`
subcommands (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 to `ConfigClient::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 publish`
- `tenant create`, `tenant list`, `tenant health`, `tenant install`
- `okd 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>`.
```rust
#[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):
```rust
// 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:
1. **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.
2. **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).
3. **`harmony_composer` placement.** Stays separate for now. If the
use cases converge with the unified CLI, it becomes `harmony
compose`. Not a blocker.
## References
- ADR-023 principle 8 — CLI: hybrid, staged (B → C)
- ADR-023 principle 5 — deploy logic in `*-deploy` crates
- ADR draft 024 §Q5 — runtime tools in the dependency graph
- `examples/try_rust_webapp``Application` trait + feature composition
- `harmony/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)