refactor: nest infra commands under sunbeam platform

Moves up, status, apply, seed, verify, logs, get, restart, build,
check, mirror, bootstrap, k8s under `sunbeam platform <command>`.
Top-level now has 19 commands instead of 32.
This commit is contained in:
2026-03-24 15:52:44 +00:00
parent 530b2a22b8
commit b08a80d177

View File

@@ -30,102 +30,18 @@ pub struct Cli {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum Verb { pub enum Verb {
// -- Infrastructure commands (preserved) ---------------------------------- /// Platform operations (cluster, builds, deploys).
Platform {
/// Full cluster bring-up. #[command(subcommand)]
Up, action: PlatformAction,
/// Pod health (optionally scoped).
Status {
/// namespace or namespace/name
target: Option<String>,
}, },
/// kustomize build + domain subst + kubectl apply.
Apply {
/// Limit apply to one namespace.
namespace: Option<String>,
/// Apply all namespaces without confirmation.
#[arg(long = "all")]
apply_all: bool,
/// Domain suffix (e.g. sunbeam.pt).
#[arg(long, default_value = "")]
domain: String,
/// ACME email for cert-manager.
#[arg(long, default_value = "")]
email: String,
},
/// Generate/store all credentials in OpenBao.
Seed,
/// E2E VSO + OpenBao integration test.
Verify,
/// kubectl logs for a service.
Logs {
/// namespace/name
target: String,
/// Stream logs.
#[arg(short, long)]
follow: bool,
},
/// Raw kubectl get for a pod (ns/name).
Get {
/// namespace/name
target: String,
/// kubectl output format (yaml, json, wide).
#[arg(long = "kubectl-output", default_value = "yaml", value_parser = ["yaml", "json", "wide"])]
output: String,
},
/// Rolling restart of services.
Restart {
/// namespace or namespace/name
target: Option<String>,
},
/// Build an artifact.
Build {
/// What to build.
what: BuildTarget,
/// Push image to registry after building.
#[arg(long)]
push: bool,
/// Apply manifests and rollout restart after pushing (implies --push).
#[arg(long)]
deploy: bool,
/// Disable buildkitd layer cache.
#[arg(long)]
no_cache: bool,
},
/// Functional service health checks.
Check {
/// namespace or namespace/name
target: Option<String>,
},
/// Mirror container images.
Mirror,
/// Bootstrap orgs, repos, and services.
Bootstrap,
/// Manage sunbeam configuration. /// Manage sunbeam configuration.
Config { Config {
#[command(subcommand)] #[command(subcommand)]
action: Option<ConfigAction>, action: Option<ConfigAction>,
}, },
/// kubectl passthrough.
K8s {
/// arguments forwarded verbatim to kubectl
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
kubectl_args: Vec<String>,
},
/// Project management. /// Project management.
Pm { Pm {
#[command(subcommand)] #[command(subcommand)]
@@ -138,7 +54,7 @@ pub enum Verb {
/// Print version info. /// Print version info.
Version, Version,
// -- Service commands (new) ----------------------------------------------- // -- Service commands -----------------------------------------------------
/// Authentication, identity & OAuth2 management. /// Authentication, identity & OAuth2 management.
Auth { Auth {
@@ -219,6 +135,85 @@ pub enum Verb {
}, },
} }
#[derive(Subcommand, Debug)]
pub enum PlatformAction {
/// Full cluster bring-up.
Up,
/// Pod health (optionally scoped).
Status {
/// namespace or namespace/name
target: Option<String>,
},
/// Build and apply manifests.
Apply {
/// Limit apply to one namespace.
namespace: Option<String>,
/// Apply all namespaces without confirmation.
#[arg(long = "all")]
apply_all: bool,
/// Domain suffix (e.g. sunbeam.pt).
#[arg(long, default_value = "")]
domain: String,
/// ACME email for cert-manager.
#[arg(long, default_value = "")]
email: String,
},
/// Seed credentials and secrets.
Seed,
/// End-to-end integration test.
Verify,
/// View service logs.
Logs {
/// namespace/name
target: String,
/// Stream logs.
#[arg(short, long)]
follow: bool,
},
/// Get a resource (ns/name).
Get {
/// namespace/name
target: String,
/// Output format (yaml, json, wide).
#[arg(long = "kubectl-output", default_value = "yaml", value_parser = ["yaml", "json", "wide"])]
output: String,
},
/// Rolling restart of services.
Restart {
/// namespace or namespace/name
target: Option<String>,
},
/// Build an artifact.
Build {
/// What to build.
what: BuildTarget,
/// Push image to registry after building.
#[arg(long)]
push: bool,
/// Apply manifests and rollout restart after pushing (implies --push).
#[arg(long)]
deploy: bool,
/// Disable layer cache.
#[arg(long)]
no_cache: bool,
},
/// Service health checks.
Check {
/// namespace or namespace/name
target: Option<String>,
},
/// Mirror container images.
Mirror,
/// Bootstrap orgs, repos, and services.
Bootstrap,
/// kubectl passthrough.
K8s {
/// arguments forwarded verbatim to kubectl
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
kubectl_args: Vec<String>,
},
}
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum PmAction { pub enum PmAction {
/// List tickets across Planka and Gitea. /// List tickets across Planka and Gitea.
@@ -313,16 +308,16 @@ mod tests {
// 1. test_up // 1. test_up
#[test] #[test]
fn test_up() { fn test_up() {
let cli = parse(&["sunbeam", "up"]); let cli = parse(&["sunbeam", "platform", "up"]);
assert!(matches!(cli.verb, Some(Verb::Up))); assert!(matches!(cli.verb, Some(Verb::Platform { action: PlatformAction::Up })));
} }
// 2. test_status_no_target // 2. test_status_no_target
#[test] #[test]
fn test_status_no_target() { fn test_status_no_target() {
let cli = parse(&["sunbeam", "status"]); let cli = parse(&["sunbeam", "platform", "status"]);
match cli.verb { match cli.verb {
Some(Verb::Status { target }) => assert!(target.is_none()), Some(Verb::Platform { action: PlatformAction::Status { target } }) => assert!(target.is_none()),
_ => panic!("expected Status"), _ => panic!("expected Status"),
} }
} }
@@ -330,9 +325,9 @@ mod tests {
// 3. test_status_with_namespace // 3. test_status_with_namespace
#[test] #[test]
fn test_status_with_namespace() { fn test_status_with_namespace() {
let cli = parse(&["sunbeam", "status", "ory"]); let cli = parse(&["sunbeam", "platform", "status", "ory"]);
match cli.verb { match cli.verb {
Some(Verb::Status { target }) => assert_eq!(target.unwrap(), "ory"), Some(Verb::Platform { action: PlatformAction::Status { target } }) => assert_eq!(target.unwrap(), "ory"),
_ => panic!("expected Status"), _ => panic!("expected Status"),
} }
} }
@@ -340,9 +335,9 @@ mod tests {
// 4. test_logs_no_follow // 4. test_logs_no_follow
#[test] #[test]
fn test_logs_no_follow() { fn test_logs_no_follow() {
let cli = parse(&["sunbeam", "logs", "ory/kratos"]); let cli = parse(&["sunbeam", "platform", "logs", "ory/kratos"]);
match cli.verb { match cli.verb {
Some(Verb::Logs { target, follow }) => { Some(Verb::Platform { action: PlatformAction::Logs { target, follow } }) => {
assert_eq!(target, "ory/kratos"); assert_eq!(target, "ory/kratos");
assert!(!follow); assert!(!follow);
} }
@@ -353,9 +348,9 @@ mod tests {
// 5. test_logs_follow_short // 5. test_logs_follow_short
#[test] #[test]
fn test_logs_follow_short() { fn test_logs_follow_short() {
let cli = parse(&["sunbeam", "logs", "ory/kratos", "-f"]); let cli = parse(&["sunbeam", "platform", "logs", "ory/kratos", "-f"]);
match cli.verb { match cli.verb {
Some(Verb::Logs { follow, .. }) => assert!(follow), Some(Verb::Platform { action: PlatformAction::Logs { follow, .. } }) => assert!(follow),
_ => panic!("expected Logs"), _ => panic!("expected Logs"),
} }
} }
@@ -363,9 +358,9 @@ mod tests {
// 6. test_build_proxy // 6. test_build_proxy
#[test] #[test]
fn test_build_proxy() { fn test_build_proxy() {
let cli = parse(&["sunbeam", "build", "proxy"]); let cli = parse(&["sunbeam", "platform", "build", "proxy"]);
match cli.verb { match cli.verb {
Some(Verb::Build { what, push, deploy, no_cache }) => { Some(Verb::Platform { action: PlatformAction::Build { what, push, deploy, no_cache } }) => {
assert!(matches!(what, BuildTarget::Proxy)); assert!(matches!(what, BuildTarget::Proxy));
assert!(!push); assert!(!push);
assert!(!deploy); assert!(!deploy);
@@ -378,9 +373,9 @@ mod tests {
// 7. test_build_deploy_flag // 7. test_build_deploy_flag
#[test] #[test]
fn test_build_deploy_flag() { fn test_build_deploy_flag() {
let cli = parse(&["sunbeam", "build", "proxy", "--deploy"]); let cli = parse(&["sunbeam", "platform", "build", "proxy", "--deploy"]);
match cli.verb { match cli.verb {
Some(Verb::Build { deploy, push, no_cache, .. }) => { Some(Verb::Platform { action: PlatformAction::Build { deploy, push, no_cache, .. } }) => {
assert!(deploy); assert!(deploy);
// clap does not imply --push; that logic is in dispatch() // clap does not imply --push; that logic is in dispatch()
assert!(!push); assert!(!push);
@@ -393,16 +388,16 @@ mod tests {
// 8. test_build_invalid_target // 8. test_build_invalid_target
#[test] #[test]
fn test_build_invalid_target() { fn test_build_invalid_target() {
let result = Cli::try_parse_from(&["sunbeam", "build", "notavalidtarget"]); let result = Cli::try_parse_from(&["sunbeam", "platform", "build", "notavalidtarget"]);
assert!(result.is_err()); assert!(result.is_err());
} }
// 12. test_apply_no_namespace // 12. test_apply_no_namespace
#[test] #[test]
fn test_apply_no_namespace() { fn test_apply_no_namespace() {
let cli = parse(&["sunbeam", "apply"]); let cli = parse(&["sunbeam", "platform", "apply"]);
match cli.verb { match cli.verb {
Some(Verb::Apply { namespace, .. }) => assert!(namespace.is_none()), Some(Verb::Platform { action: PlatformAction::Apply { namespace, .. } }) => assert!(namespace.is_none()),
_ => panic!("expected Apply"), _ => panic!("expected Apply"),
} }
} }
@@ -410,9 +405,9 @@ mod tests {
// 13. test_apply_with_namespace // 13. test_apply_with_namespace
#[test] #[test]
fn test_apply_with_namespace() { fn test_apply_with_namespace() {
let cli = parse(&["sunbeam", "apply", "lasuite"]); let cli = parse(&["sunbeam", "platform", "apply", "lasuite"]);
match cli.verb { match cli.verb {
Some(Verb::Apply { namespace, .. }) => assert_eq!(namespace.unwrap(), "lasuite"), Some(Verb::Platform { action: PlatformAction::Apply { namespace, .. } }) => assert_eq!(namespace.unwrap(), "lasuite"),
_ => panic!("expected Apply"), _ => panic!("expected Apply"),
} }
} }
@@ -463,9 +458,9 @@ mod tests {
// 17. test_get_json_output // 17. test_get_json_output
#[test] #[test]
fn test_get_json_output() { fn test_get_json_output() {
let cli = parse(&["sunbeam", "get", "ory/kratos-abc", "--kubectl-output", "json"]); let cli = parse(&["sunbeam", "platform", "get", "ory/kratos-abc", "--kubectl-output", "json"]);
match cli.verb { match cli.verb {
Some(Verb::Get { target, output }) => { Some(Verb::Platform { action: PlatformAction::Get { target, output } }) => {
assert_eq!(target, "ory/kratos-abc"); assert_eq!(target, "ory/kratos-abc");
assert_eq!(output, "json"); assert_eq!(output, "json");
} }
@@ -476,9 +471,9 @@ mod tests {
// 18. test_check_with_target // 18. test_check_with_target
#[test] #[test]
fn test_check_with_target() { fn test_check_with_target() {
let cli = parse(&["sunbeam", "check", "devtools"]); let cli = parse(&["sunbeam", "platform", "check", "devtools"]);
match cli.verb { match cli.verb {
Some(Verb::Check { target }) => assert_eq!(target.unwrap(), "devtools"), Some(Verb::Platform { action: PlatformAction::Check { target } }) => assert_eq!(target.unwrap(), "devtools"),
_ => panic!("expected Check"), _ => panic!("expected Check"),
} }
} }
@@ -486,9 +481,9 @@ mod tests {
// 19. test_build_messages_components // 19. test_build_messages_components
#[test] #[test]
fn test_build_messages_backend() { fn test_build_messages_backend() {
let cli = parse(&["sunbeam", "build", "messages-backend"]); let cli = parse(&["sunbeam", "platform", "build", "messages-backend"]);
match cli.verb { match cli.verb {
Some(Verb::Build { what, .. }) => { Some(Verb::Platform { action: PlatformAction::Build { what, .. } }) => {
assert!(matches!(what, BuildTarget::MessagesBackend)); assert!(matches!(what, BuildTarget::MessagesBackend));
} }
_ => panic!("expected Build"), _ => panic!("expected Build"),
@@ -497,9 +492,9 @@ mod tests {
#[test] #[test]
fn test_build_messages_frontend() { fn test_build_messages_frontend() {
let cli = parse(&["sunbeam", "build", "messages-frontend"]); let cli = parse(&["sunbeam", "platform", "build", "messages-frontend"]);
match cli.verb { match cli.verb {
Some(Verb::Build { what, .. }) => { Some(Verb::Platform { action: PlatformAction::Build { what, .. } }) => {
assert!(matches!(what, BuildTarget::MessagesFrontend)); assert!(matches!(what, BuildTarget::MessagesFrontend));
} }
_ => panic!("expected Build"), _ => panic!("expected Build"),
@@ -508,9 +503,9 @@ mod tests {
#[test] #[test]
fn test_build_messages_mta_in() { fn test_build_messages_mta_in() {
let cli = parse(&["sunbeam", "build", "messages-mta-in"]); let cli = parse(&["sunbeam", "platform", "build", "messages-mta-in"]);
match cli.verb { match cli.verb {
Some(Verb::Build { what, .. }) => { Some(Verb::Platform { action: PlatformAction::Build { what, .. } }) => {
assert!(matches!(what, BuildTarget::MessagesMtaIn)); assert!(matches!(what, BuildTarget::MessagesMtaIn));
} }
_ => panic!("expected Build"), _ => panic!("expected Build"),
@@ -519,9 +514,9 @@ mod tests {
#[test] #[test]
fn test_build_messages_mta_out() { fn test_build_messages_mta_out() {
let cli = parse(&["sunbeam", "build", "messages-mta-out"]); let cli = parse(&["sunbeam", "platform", "build", "messages-mta-out"]);
match cli.verb { match cli.verb {
Some(Verb::Build { what, .. }) => { Some(Verb::Platform { action: PlatformAction::Build { what, .. } }) => {
assert!(matches!(what, BuildTarget::MessagesMtaOut)); assert!(matches!(what, BuildTarget::MessagesMtaOut));
} }
_ => panic!("expected Build"), _ => panic!("expected Build"),
@@ -530,9 +525,9 @@ mod tests {
#[test] #[test]
fn test_build_messages_mpa() { fn test_build_messages_mpa() {
let cli = parse(&["sunbeam", "build", "messages-mpa"]); let cli = parse(&["sunbeam", "platform", "build", "messages-mpa"]);
match cli.verb { match cli.verb {
Some(Verb::Build { what, .. }) => { Some(Verb::Platform { action: PlatformAction::Build { what, .. } }) => {
assert!(matches!(what, BuildTarget::MessagesMpa)); assert!(matches!(what, BuildTarget::MessagesMpa));
} }
_ => panic!("expected Build"), _ => panic!("expected Build"),
@@ -541,9 +536,9 @@ mod tests {
#[test] #[test]
fn test_build_messages_socks_proxy() { fn test_build_messages_socks_proxy() {
let cli = parse(&["sunbeam", "build", "messages-socks-proxy"]); let cli = parse(&["sunbeam", "platform", "build", "messages-socks-proxy"]);
match cli.verb { match cli.verb {
Some(Verb::Build { what, .. }) => { Some(Verb::Platform { action: PlatformAction::Build { what, .. } }) => {
assert!(matches!(what, BuildTarget::MessagesSocksProxy)); assert!(matches!(what, BuildTarget::MessagesSocksProxy));
} }
_ => panic!("expected Build"), _ => panic!("expected Build"),
@@ -663,12 +658,12 @@ mod tests {
#[test] #[test]
fn test_infra_commands_preserved() { fn test_infra_commands_preserved() {
// Verify all old infra commands still parse // Verify all old infra commands still parse under platform
assert!(matches!(parse(&["sunbeam", "up"]).verb, Some(Verb::Up))); assert!(matches!(parse(&["sunbeam", "platform", "up"]).verb, Some(Verb::Platform { action: PlatformAction::Up })));
assert!(matches!(parse(&["sunbeam", "seed"]).verb, Some(Verb::Seed))); assert!(matches!(parse(&["sunbeam", "platform", "seed"]).verb, Some(Verb::Platform { action: PlatformAction::Seed })));
assert!(matches!(parse(&["sunbeam", "verify"]).verb, Some(Verb::Verify))); assert!(matches!(parse(&["sunbeam", "platform", "verify"]).verb, Some(Verb::Platform { action: PlatformAction::Verify })));
assert!(matches!(parse(&["sunbeam", "mirror"]).verb, Some(Verb::Mirror))); assert!(matches!(parse(&["sunbeam", "platform", "mirror"]).verb, Some(Verb::Platform { action: PlatformAction::Mirror })));
assert!(matches!(parse(&["sunbeam", "bootstrap"]).verb, Some(Verb::Bootstrap))); assert!(matches!(parse(&["sunbeam", "platform", "bootstrap"]).verb, Some(Verb::Platform { action: PlatformAction::Bootstrap })));
assert!(matches!(parse(&["sunbeam", "update"]).verb, Some(Verb::Update))); assert!(matches!(parse(&["sunbeam", "update"]).verb, Some(Verb::Update)));
assert!(matches!(parse(&["sunbeam", "version"]).verb, Some(Verb::Version))); assert!(matches!(parse(&["sunbeam", "version"]).verb, Some(Verb::Version)));
} }
@@ -708,18 +703,19 @@ pub async fn dispatch() -> Result<()> {
Ok(()) Ok(())
} }
Some(Verb::Up) => sunbeam_sdk::cluster::cmd_up().await, Some(Verb::Platform { action }) => match action {
PlatformAction::Up => sunbeam_sdk::cluster::cmd_up().await,
Some(Verb::Status { target }) => { PlatformAction::Status { target } => {
sunbeam_sdk::services::cmd_status(target.as_deref()).await sunbeam_sdk::services::cmd_status(target.as_deref()).await
} }
Some(Verb::Apply { PlatformAction::Apply {
namespace, namespace,
apply_all, apply_all,
domain, domain,
email, email,
}) => { } => {
let is_production = !sunbeam_sdk::config::active_context().ssh_host.is_empty(); let is_production = !sunbeam_sdk::config::active_context().ssh_host.is_empty();
let env_str = if is_production { "production" } else { "local" }; let env_str = if is_production { "production" } else { "local" };
let domain = if domain.is_empty() { let domain = if domain.is_empty() {
@@ -751,34 +747,39 @@ pub async fn dispatch() -> Result<()> {
sunbeam_sdk::manifests::cmd_apply(&env_str, &domain, &email, &ns).await sunbeam_sdk::manifests::cmd_apply(&env_str, &domain, &email, &ns).await
} }
Some(Verb::Seed) => sunbeam_sdk::secrets::cmd_seed().await, PlatformAction::Seed => sunbeam_sdk::secrets::cmd_seed().await,
Some(Verb::Verify) => sunbeam_sdk::secrets::cmd_verify().await, PlatformAction::Verify => sunbeam_sdk::secrets::cmd_verify().await,
Some(Verb::Logs { target, follow }) => { PlatformAction::Logs { target, follow } => {
sunbeam_sdk::services::cmd_logs(&target, follow).await sunbeam_sdk::services::cmd_logs(&target, follow).await
} }
Some(Verb::Get { target, output }) => { PlatformAction::Get { target, output } => {
sunbeam_sdk::services::cmd_get(&target, &output).await sunbeam_sdk::services::cmd_get(&target, &output).await
} }
Some(Verb::Restart { target }) => { PlatformAction::Restart { target } => {
sunbeam_sdk::services::cmd_restart(target.as_deref()).await sunbeam_sdk::services::cmd_restart(target.as_deref()).await
} }
Some(Verb::Build { what, push, deploy, no_cache }) => { PlatformAction::Build { what, push, deploy, no_cache } => {
let push = push || deploy; let push = push || deploy;
sunbeam_sdk::images::cmd_build(&what, push, deploy, no_cache).await sunbeam_sdk::images::cmd_build(&what, push, deploy, no_cache).await
} }
Some(Verb::Check { target }) => { PlatformAction::Check { target } => {
sunbeam_sdk::checks::cmd_check(target.as_deref()).await sunbeam_sdk::checks::cmd_check(target.as_deref()).await
} }
Some(Verb::Mirror) => sunbeam_sdk::images::cmd_mirror().await, PlatformAction::Mirror => sunbeam_sdk::images::cmd_mirror().await,
Some(Verb::Bootstrap) => sunbeam_sdk::gitea::cmd_bootstrap().await, PlatformAction::Bootstrap => sunbeam_sdk::gitea::cmd_bootstrap().await,
PlatformAction::K8s { kubectl_args } => {
sunbeam_sdk::kube::cmd_k8s(&kubectl_args).await
}
},
Some(Verb::Config { action }) => match action { Some(Verb::Config { action }) => match action {
None => { None => {
@@ -877,10 +878,6 @@ pub async fn dispatch() -> Result<()> {
Some(ConfigAction::Clear) => sunbeam_sdk::config::clear_config(), Some(ConfigAction::Clear) => sunbeam_sdk::config::clear_config(),
}, },
Some(Verb::K8s { kubectl_args }) => {
sunbeam_sdk::kube::cmd_k8s(&kubectl_args).await
}
Some(Verb::Auth { action }) => { Some(Verb::Auth { action }) => {
let sc = sunbeam_sdk::client::SunbeamClient::from_context( let sc = sunbeam_sdk::client::SunbeamClient::from_context(
&sunbeam_sdk::config::active_context(), &sunbeam_sdk::config::active_context(),