diff --git a/.gitea/workflows/harmony-fleet-operator.yaml b/.gitea/workflows/harmony-fleet-operator.yaml index 674b2542..c900d095 100644 --- a/.gitea/workflows/harmony-fleet-operator.yaml +++ b/.gitea/workflows/harmony-fleet-operator.yaml @@ -4,7 +4,7 @@ name: harmony-fleet-operator — release # published chart is `harmony apply` # (harmony-fleet-deploy --operator-chart-version), run manually today; a # CD job lands once the cluster KUBECONFIG + NATS secrets are provisioned. -# Tag parsing lives in Rust (harmony-fleet-release), not in YAML. +# Tag parsing lives in Rust (harmony-fleet-publish), not in YAML. on: push: tags: @@ -48,4 +48,4 @@ jobs: - name: Build + push image and chart env: TAG: ${{ inputs.tag || github.ref_name }} - run: cargo run --release -p harmony-fleet-deploy --bin harmony-fleet-release -- --from-tag "$TAG" + run: cargo run --release -p harmony-fleet-deploy --bin harmony-fleet-publish -- --from-tag "$TAG" diff --git a/ROADMAP/13-unified-cli.md b/ROADMAP/13-unified-cli.md new file mode 100644 index 00000000..2241430d --- /dev/null +++ b/ROADMAP/13-unified-cli.md @@ -0,0 +1,281 @@ +# 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`. +- `harmony_cli::run(Inventory, Topology, Vec, Option)` — + 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] [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`. + +```rust +#[derive(Parser)] +struct Cli { + #[arg(long, env = "HARMONY_CONFIG_NAMESPACE", global = true)] + config_namespace: String, + + #[arg(long, global = true)] + kubeconfig: Option, + + #[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::("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) diff --git a/fleet/deployment-process.md b/fleet/deployment-process.md index 70e6e115..2b125e63 100644 --- a/fleet/deployment-process.md +++ b/fleet/deployment-process.md @@ -20,11 +20,11 @@ Laptop fallback (does exactly what the workflow's job does): ```sh # docker + helm must be logged in to hub.nationtech.io first. -cargo run --release -p harmony-fleet-deploy --bin harmony-fleet-release -- \ +cargo run --release -p harmony-fleet-deploy --bin harmony-fleet-publish -- \ --from-tag harmony-fleet-operator-v0.0.2 # build + package only, no push (local k3d smoke-test): -cargo run -p harmony-fleet-deploy --bin harmony-fleet-release -- \ +cargo run -p harmony-fleet-deploy --bin harmony-fleet-publish -- \ --from-tag harmony-fleet-operator-v0.0.2 --no-push ``` diff --git a/fleet/harmony-fleet-deploy/Cargo.toml b/fleet/harmony-fleet-deploy/Cargo.toml index fb855a03..1c740e33 100644 --- a/fleet/harmony-fleet-deploy/Cargo.toml +++ b/fleet/harmony-fleet-deploy/Cargo.toml @@ -15,11 +15,11 @@ path = "src/lib.rs" name = "harmony-fleet-deploy" path = "src/main.rs" -# `harmony-fleet-release --from-tag ` builds + publishes the +# `harmony-fleet-publish --from-tag ` builds + publishes the # operator's image + chart for a release. [[bin]] -name = "harmony-fleet-release" -path = "src/bin/harmony-fleet-release.rs" +name = "harmony-fleet-publish" +path = "src/bin/harmony-fleet-publish.rs" [dependencies] harmony = { path = "../../harmony", features = ["podman"] } diff --git a/fleet/harmony-fleet-deploy/src/bin/harmony-fleet-release.rs b/fleet/harmony-fleet-deploy/src/bin/harmony-fleet-publish.rs similarity index 91% rename from fleet/harmony-fleet-deploy/src/bin/harmony-fleet-release.rs rename to fleet/harmony-fleet-deploy/src/bin/harmony-fleet-publish.rs index a49b7a7c..00298cec 100644 --- a/fleet/harmony-fleet-deploy/src/bin/harmony-fleet-release.rs +++ b/fleet/harmony-fleet-deploy/src/bin/harmony-fleet-publish.rs @@ -1,4 +1,4 @@ -//! `harmony-fleet-release` — build + publish the operator image + chart +//! `harmony-fleet-publish` — build + publish the operator image + chart //! for a tagged release. `docker` / `helm` must be on PATH and logged in //! to the registry (CI's login actions; dev's manual login). @@ -8,7 +8,7 @@ use harmony_fleet_deploy::release::{release_operator, version_from_tag}; #[derive(Parser, Debug)] #[command( - name = "harmony-fleet-release", + name = "harmony-fleet-publish", about = "Build + publish the operator image + chart for a tagged release" )] struct Cli { diff --git a/fleet/harmony-fleet-deploy/src/main.rs b/fleet/harmony-fleet-deploy/src/main.rs index f0afc9a7..ed69d401 100644 --- a/fleet/harmony-fleet-deploy/src/main.rs +++ b/fleet/harmony-fleet-deploy/src/main.rs @@ -18,6 +18,7 @@ use harmony_config::ConfigClient; use harmony_fleet_deploy::{ FleetDeployConfig, FleetDeploySecrets, FleetOperatorScore, version_from_tag, }; +use tracing::info; #[derive(Parser, Debug)] #[command( @@ -71,6 +72,7 @@ impl CliConfig { #[tokio::main] async fn main() -> Result<()> { + harmony_cli::cli_logger::init(); let cli = CliConfig::parse(); let version = cli.chart_version()?;