refactor(fleet-deploy): rename harmony-fleet-release to harmony-fleet-publish #315
@@ -4,7 +4,7 @@ name: harmony-fleet-operator — release
|
|||||||
# published chart is `harmony apply`
|
# published chart is `harmony apply`
|
||||||
# (harmony-fleet-deploy --operator-chart-version), run manually today; a
|
# (harmony-fleet-deploy --operator-chart-version), run manually today; a
|
||||||
# CD job lands once the cluster KUBECONFIG + NATS secrets are provisioned.
|
# 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:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@@ -48,4 +48,4 @@ jobs:
|
|||||||
- name: Build + push image and chart
|
- name: Build + push image and chart
|
||||||
env:
|
env:
|
||||||
TAG: ${{ inputs.tag || github.ref_name }}
|
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"
|
||||||
|
|||||||
281
ROADMAP/13-unified-cli.md
Normal file
281
ROADMAP/13-unified-cli.md
Normal file
@@ -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<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)
|
||||||
@@ -20,11 +20,11 @@ Laptop fallback (does exactly what the workflow's job does):
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
# docker + helm must be logged in to hub.nationtech.io first.
|
# 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
|
--from-tag harmony-fleet-operator-v0.0.2
|
||||||
|
|
||||||
# build + package only, no push (local k3d smoke-test):
|
# 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
|
--from-tag harmony-fleet-operator-v0.0.2 --no-push
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ path = "src/lib.rs"
|
|||||||
name = "harmony-fleet-deploy"
|
name = "harmony-fleet-deploy"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
# `harmony-fleet-release --from-tag <tag>` builds + publishes the
|
# `harmony-fleet-publish --from-tag <tag>` builds + publishes the
|
||||||
# operator's image + chart for a release.
|
# operator's image + chart for a release.
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "harmony-fleet-release"
|
name = "harmony-fleet-publish"
|
||||||
path = "src/bin/harmony-fleet-release.rs"
|
path = "src/bin/harmony-fleet-publish.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
harmony = { path = "../../harmony", features = ["podman"] }
|
harmony = { path = "../../harmony", features = ["podman"] }
|
||||||
|
|||||||
@@ -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
|
//! for a tagged release. `docker` / `helm` must be on PATH and logged in
|
||||||
//! to the registry (CI's login actions; dev's manual login).
|
//! 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)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(
|
#[command(
|
||||||
name = "harmony-fleet-release",
|
name = "harmony-fleet-publish",
|
||||||
about = "Build + publish the operator image + chart for a tagged release"
|
about = "Build + publish the operator image + chart for a tagged release"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
@@ -18,6 +18,7 @@ use harmony_config::ConfigClient;
|
|||||||
use harmony_fleet_deploy::{
|
use harmony_fleet_deploy::{
|
||||||
FleetDeployConfig, FleetDeploySecrets, FleetOperatorScore, version_from_tag,
|
FleetDeployConfig, FleetDeploySecrets, FleetOperatorScore, version_from_tag,
|
||||||
};
|
};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(
|
#[command(
|
||||||
@@ -71,6 +72,7 @@ impl CliConfig {
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
|
harmony_cli::cli_logger::init();
|
||||||
let cli = CliConfig::parse();
|
let cli = CliConfig::parse();
|
||||||
let version = cli.chart_version()?;
|
let version = cli.chart_version()?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user