7 Commits

Author SHA1 Message Date
683cec9307 release: v1.1.2
- fix(opensearch): make ML model registration idempotent
2026-03-25 18:09:25 +00:00
30dc4f9c5e fix(opensearch): make ML model registration idempotent
Reuse any existing model version (including DEPLOY_FAILED) instead of
registering a new copy. Prevents accumulation of stale model chunks
in .plugins-ml-model when OpenSearch restarts between applies.
2026-03-25 18:04:28 +00:00
3d2d16d53e feat(secrets): add xchacha20-poly1305 cipher key seeding for Kratos
Add rand_alphanum() using OsRng for generating fixed-length
alphanumeric secrets. Seed secrets-cipher (32 chars) into the
kratos KV path for at-rest encryption of OIDC tokens.
2026-03-24 20:51:13 +00:00
80ab6d6113 feat: enable Meet external API, fix SDK path
- Meet config: EXTERNAL_API_ENABLED=True
- Meet backend: added lasuite-resource-server configmap + RS creds
- Pingora: added /external-api/ route for Meet
- SDK: fixed Meet URL to use /external-api/ (hyphenated)

NOTE: Meet RS requires ES256 tokens + lasuite_meet scope — CLI
tokens use RS256 + generic scopes. Needs RS config adjustment.
2026-03-24 17:03:55 +00:00
b08a80d177 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.
2026-03-24 15:52:44 +00:00
530b2a22b8 chore: remove solution branding from CLI help text 2026-03-24 15:44:39 +00:00
6a2b62dc42 refactor: remove bao, docs, and people subcommands
- bao: replaced by `sunbeam vault` with proper JWT auth
- docs: La Suite Docs not ready for production
- people: La Suite People not ready for production
2026-03-24 15:40:58 +00:00
8 changed files with 253 additions and 283 deletions

View File

@@ -1,5 +1,12 @@
# Changelog # Changelog
## v1.1.2
- 30dc4f9 fix(opensearch): make ML model registration idempotent
- 3d2d16d feat(secrets): add xchacha20-poly1305 cipher key seeding for Kratos
- 80ab6d6 feat: enable Meet external API, fix SDK path
- b08a80d refactor: nest infra commands under `sunbeam platform`
## v1.1.1 ## v1.1.1
- cd80a57 fix: DynamicBearer auth, retry on 500/429, upload resilience - cd80a57 fix: DynamicBearer auth, retry on 500/429, upload resilience

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "sunbeam-sdk" name = "sunbeam-sdk"
version = "1.1.1" version = "1.1.2"
edition = "2024" edition = "2024"
description = "Sunbeam Studios SDK, CLI, and ecosystem integrations" description = "Sunbeam Studios SDK, CLI, and ecosystem integrations"
repository = "https://src.sunbeam.pt/studio/cli" repository = "https://src.sunbeam.pt/studio/cli"

View File

@@ -456,7 +456,7 @@ impl SunbeamClient {
pub async fn meet(&self) -> Result<&crate::lasuite::MeetClient> { pub async fn meet(&self) -> Result<&crate::lasuite::MeetClient> {
self.sso_token().await?; self.sso_token().await?;
self.meet.get_or_try_init(|| async { self.meet.get_or_try_init(|| async {
let url = format!("https://meet.{}/external_api/v1.0", self.domain); let url = format!("https://meet.{}/external-api/v1.0", self.domain);
Ok(crate::lasuite::MeetClient::from_parts(url, AuthMethod::DynamicBearer)) Ok(crate::lasuite::MeetClient::from_parts(url, AuthMethod::DynamicBearer))
}).await }).await
} }

View File

@@ -617,10 +617,14 @@ async fn ensure_opensearch_ml() {
already_deployed = true; already_deployed = true;
break; break;
} }
"REGISTERED" | "DEPLOYING" => { // Any existing model (even DEPLOY_FAILED) — reuse it instead of
model_id = Some(id.to_string()); // registering a new version. This prevents accumulating stale
// copies in .plugins-ml-model when the pod restarts.
_ => {
if model_id.is_none() && !id.is_empty() {
model_id = Some(id.to_string());
}
} }
_ => {}
} }
} }

View File

@@ -102,6 +102,15 @@ fn rand_token_n(n: usize) -> String {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf) base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(buf)
} }
/// Generate an alphanumeric random string of exactly `n` characters.
/// Used for secrets that require a fixed character length (e.g. xchacha20-poly1305 cipher keys).
pub(crate) fn rand_alphanum(n: usize) -> String {
use rand::rngs::OsRng;
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
(0..n).map(|_| CHARSET[OsRng.gen_range(0..CHARSET.len())] as char).collect()
}
// ── Port-forward helper ───────────────────────────────────────────────────── // ── Port-forward helper ─────────────────────────────────────────────────────
/// Port-forward guard — cancels the background forwarder on drop. /// Port-forward guard — cancels the background forwarder on drop.

View File

@@ -11,8 +11,8 @@ use crate::openbao::BaoClient;
use crate::output::{ok, warn}; use crate::output::{ok, warn};
use super::{ use super::{
gen_dkim_key_pair, gen_fernet_key, port_forward, rand_token, rand_token_n, scw_config, gen_dkim_key_pair, gen_fernet_key, port_forward, rand_alphanum, rand_token, rand_token_n,
wait_pod_running, delete_resource, GITEA_ADMIN_USER, SMTP_URI, scw_config, wait_pod_running, delete_resource, GITEA_ADMIN_USER, SMTP_URI,
}; };
/// Internal result from seed_openbao, used by cmd_seed. /// Internal result from seed_openbao, used by cmd_seed.
@@ -238,12 +238,14 @@ pub async fn seed_openbao() -> Result<Option<SeedResult>> {
.await?; .await?;
let smtp_uri_fn = || SMTP_URI.to_string(); let smtp_uri_fn = || SMTP_URI.to_string();
let cipher_fn = || rand_alphanum(32);
let kratos = get_or_create( let kratos = get_or_create(
&bao, &bao,
"kratos", "kratos",
&[ &[
("secrets-default", &rand_token as &dyn Fn() -> String), ("secrets-default", &rand_token as &dyn Fn() -> String),
("secrets-cookie", &rand_token), ("secrets-cookie", &rand_token),
("secrets-cipher", &cipher_fn),
("smtp-connection-uri", &smtp_uri_fn), ("smtp-connection-uri", &smtp_uri_fn),
], ],
&mut dirty_paths, &mut dirty_paths,

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "sunbeam" name = "sunbeam"
version = "1.1.1" version = "1.1.2"
edition = "2024" edition = "2024"
description = "Sunbeam Studios SDK, CLI, and ecosystem integrations" description = "Sunbeam Studios SDK, CLI, and ecosystem integrations"

View File

@@ -5,7 +5,7 @@ use clap::{Parser, Subcommand};
/// Sunbeam local dev stack manager. /// Sunbeam local dev stack manager.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(name = "sunbeam", about = "Sunbeam local dev stack manager")] #[command(name = "sunbeam", about = "Sunbeam Studios CLI")]
pub struct Cli { pub struct Cli {
/// Named context to use (overrides current-context from config). /// Named context to use (overrides current-context from config).
#[arg(long)] #[arg(long)]
@@ -30,18 +30,121 @@ pub struct Cli {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum Verb { pub enum Verb {
// -- Infrastructure commands (preserved) ---------------------------------- /// Platform operations (cluster, builds, deploys).
Platform {
#[command(subcommand)]
action: PlatformAction,
},
/// Manage sunbeam configuration.
Config {
#[command(subcommand)]
action: Option<ConfigAction>,
},
/// Project management.
Pm {
#[command(subcommand)]
action: Option<PmAction>,
},
/// Self-update from latest mainline commit.
Update,
/// Print version info.
Version,
// -- Service commands -----------------------------------------------------
/// Authentication, identity & OAuth2 management.
Auth {
#[command(subcommand)]
action: sunbeam_sdk::identity::cli::AuthCommand,
},
/// Version control.
Vcs {
#[command(subcommand)]
action: sunbeam_sdk::gitea::cli::VcsCommand,
},
/// Chat and messaging.
Chat {
#[command(subcommand)]
action: sunbeam_sdk::matrix::cli::ChatCommand,
},
/// Search engine.
Search {
#[command(subcommand)]
action: sunbeam_sdk::search::cli::SearchCommand,
},
/// Object storage.
Storage {
#[command(subcommand)]
action: sunbeam_sdk::storage::cli::StorageCommand,
},
/// Media and video.
Media {
#[command(subcommand)]
action: sunbeam_sdk::media::cli::MediaCommand,
},
/// Monitoring.
Mon {
#[command(subcommand)]
action: sunbeam_sdk::monitoring::cli::MonCommand,
},
/// Secrets management.
Vault {
#[command(subcommand)]
action: sunbeam_sdk::openbao::cli::VaultCommand,
},
/// Video meetings.
Meet {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::MeetCommand,
},
/// File storage.
Drive {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::DriveCommand,
},
/// Email.
Mail {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::MailCommand,
},
/// Calendar.
Cal {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::CalCommand,
},
/// Search across services.
Find {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::FindCommand,
},
}
#[derive(Subcommand, Debug)]
pub enum PlatformAction {
/// Full cluster bring-up. /// Full cluster bring-up.
Up, Up,
/// Pod health (optionally scoped). /// Pod health (optionally scoped).
Status { Status {
/// namespace or namespace/name /// namespace or namespace/name
target: Option<String>, target: Option<String>,
}, },
/// Build and apply manifests.
/// kustomize build + domain subst + kubectl apply.
Apply { Apply {
/// Limit apply to one namespace. /// Limit apply to one namespace.
namespace: Option<String>, namespace: Option<String>,
@@ -55,14 +158,11 @@ pub enum Verb {
#[arg(long, default_value = "")] #[arg(long, default_value = "")]
email: String, email: String,
}, },
/// Seed credentials and secrets.
/// Generate/store all credentials in OpenBao.
Seed, Seed,
/// End-to-end integration test.
/// E2E VSO + OpenBao integration test.
Verify, Verify,
/// View service logs.
/// kubectl logs for a service.
Logs { Logs {
/// namespace/name /// namespace/name
target: String, target: String,
@@ -70,22 +170,19 @@ pub enum Verb {
#[arg(short, long)] #[arg(short, long)]
follow: bool, follow: bool,
}, },
/// Get a resource (ns/name).
/// Raw kubectl get for a pod (ns/name).
Get { Get {
/// namespace/name /// namespace/name
target: String, target: String,
/// kubectl output format (yaml, json, wide). /// Output format (yaml, json, wide).
#[arg(long = "kubectl-output", default_value = "yaml", value_parser = ["yaml", "json", "wide"])] #[arg(long = "kubectl-output", default_value = "yaml", value_parser = ["yaml", "json", "wide"])]
output: String, output: String,
}, },
/// Rolling restart of services. /// Rolling restart of services.
Restart { Restart {
/// namespace or namespace/name /// namespace or namespace/name
target: Option<String>, target: Option<String>,
}, },
/// Build an artifact. /// Build an artifact.
Build { Build {
/// What to build. /// What to build.
@@ -96,146 +193,25 @@ pub enum Verb {
/// Apply manifests and rollout restart after pushing (implies --push). /// Apply manifests and rollout restart after pushing (implies --push).
#[arg(long)] #[arg(long)]
deploy: bool, deploy: bool,
/// Disable buildkitd layer cache. /// Disable layer cache.
#[arg(long)] #[arg(long)]
no_cache: bool, no_cache: bool,
}, },
/// Service health checks.
/// Functional service health checks.
Check { Check {
/// namespace or namespace/name /// namespace or namespace/name
target: Option<String>, target: Option<String>,
}, },
/// Mirror container images.
/// Mirror amd64-only La Suite images.
Mirror, Mirror,
/// Bootstrap orgs, repos, and services.
/// Create Gitea orgs/repos; bootstrap services.
Bootstrap, Bootstrap,
/// Manage sunbeam configuration.
Config {
#[command(subcommand)]
action: Option<ConfigAction>,
},
/// kubectl passthrough. /// kubectl passthrough.
K8s { K8s {
/// arguments forwarded verbatim to kubectl /// arguments forwarded verbatim to kubectl
#[arg(trailing_var_arg = true, allow_hyphen_values = true)] #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
kubectl_args: Vec<String>, kubectl_args: Vec<String>,
}, },
/// bao CLI passthrough (runs inside OpenBao pod with root token).
Bao {
/// arguments forwarded verbatim to bao
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
bao_args: Vec<String>,
},
/// Project management across Planka and Gitea.
Pm {
#[command(subcommand)]
action: Option<PmAction>,
},
/// Self-update from latest mainline commit.
Update,
/// Print version info.
Version,
// -- Service commands (new) -----------------------------------------------
/// Authentication, identity & OAuth2 management.
Auth {
#[command(subcommand)]
action: sunbeam_sdk::identity::cli::AuthCommand,
},
/// Version control (Gitea).
Vcs {
#[command(subcommand)]
action: sunbeam_sdk::gitea::cli::VcsCommand,
},
/// Chat / messaging (Matrix).
Chat {
#[command(subcommand)]
action: sunbeam_sdk::matrix::cli::ChatCommand,
},
/// Search engine (OpenSearch).
Search {
#[command(subcommand)]
action: sunbeam_sdk::search::cli::SearchCommand,
},
/// Object storage (S3).
Storage {
#[command(subcommand)]
action: sunbeam_sdk::storage::cli::StorageCommand,
},
/// Media / video (LiveKit).
Media {
#[command(subcommand)]
action: sunbeam_sdk::media::cli::MediaCommand,
},
/// Monitoring (Prometheus, Loki, Grafana).
Mon {
#[command(subcommand)]
action: sunbeam_sdk::monitoring::cli::MonCommand,
},
/// Secrets management (OpenBao/Vault).
Vault {
#[command(subcommand)]
action: sunbeam_sdk::openbao::cli::VaultCommand,
},
/// People / contacts (La Suite).
People {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::PeopleCommand,
},
/// Documents (La Suite).
Docs {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::DocsCommand,
},
/// Video meetings (La Suite).
Meet {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::MeetCommand,
},
/// File storage (La Suite).
Drive {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::DriveCommand,
},
/// Email (La Suite).
Mail {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::MailCommand,
},
/// Calendar (La Suite).
Cal {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::CalCommand,
},
/// Search across La Suite services.
Find {
#[command(subcommand)]
action: sunbeam_sdk::lasuite::cli::FindCommand,
},
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
@@ -332,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"),
} }
} }
@@ -349,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"),
} }
} }
@@ -359,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);
} }
@@ -372,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"),
} }
} }
@@ -382,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);
@@ -397,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);
@@ -412,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"),
} }
} }
@@ -429,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"),
} }
} }
@@ -482,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");
} }
@@ -495,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"),
} }
} }
@@ -505,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"),
@@ -516,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"),
@@ -527,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"),
@@ -538,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"),
@@ -549,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"),
@@ -560,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"),
@@ -643,18 +619,6 @@ mod tests {
assert!(matches!(cli.verb, Some(Verb::Vault { .. }))); assert!(matches!(cli.verb, Some(Verb::Vault { .. })));
} }
#[test]
fn test_people_contact_list() {
let cli = parse(&["sunbeam", "people", "contact", "list"]);
assert!(matches!(cli.verb, Some(Verb::People { .. })));
}
#[test]
fn test_docs_document_list() {
let cli = parse(&["sunbeam", "docs", "document", "list"]);
assert!(matches!(cli.verb, Some(Verb::Docs { .. })));
}
#[test] #[test]
fn test_meet_room_list() { fn test_meet_room_list() {
let cli = parse(&["sunbeam", "meet", "room", "list"]); let cli = parse(&["sunbeam", "meet", "room", "list"]);
@@ -694,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)));
} }
@@ -739,77 +703,83 @@ 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 {
namespace,
apply_all,
domain,
email,
}) => {
let is_production = !sunbeam_sdk::config::active_context().ssh_host.is_empty();
let env_str = if is_production { "production" } else { "local" };
let domain = if domain.is_empty() {
cli.domain.clone()
} else {
domain
};
let email = if email.is_empty() {
cli.email.clone()
} else {
email
};
let ns = namespace.unwrap_or_default();
// Production full-apply requires --all or confirmation
if is_production && ns.is_empty() && !apply_all {
sunbeam_sdk::output::warn(
"This will apply ALL namespaces to production.",
);
eprint!(" Continue? [y/N] ");
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if !matches!(answer.trim().to_lowercase().as_str(), "y" | "yes") {
println!("Aborted.");
return Ok(());
}
} }
sunbeam_sdk::manifests::cmd_apply(&env_str, &domain, &email, &ns).await PlatformAction::Apply {
} namespace,
apply_all,
domain,
email,
} => {
let is_production = !sunbeam_sdk::config::active_context().ssh_host.is_empty();
let env_str = if is_production { "production" } else { "local" };
let domain = if domain.is_empty() {
cli.domain.clone()
} else {
domain
};
let email = if email.is_empty() {
cli.email.clone()
} else {
email
};
let ns = namespace.unwrap_or_default();
Some(Verb::Seed) => sunbeam_sdk::secrets::cmd_seed().await, // Production full-apply requires --all or confirmation
if is_production && ns.is_empty() && !apply_all {
sunbeam_sdk::output::warn(
"This will apply ALL namespaces to production.",
);
eprint!(" Continue? [y/N] ");
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if !matches!(answer.trim().to_lowercase().as_str(), "y" | "yes") {
println!("Aborted.");
return Ok(());
}
}
Some(Verb::Verify) => sunbeam_sdk::secrets::cmd_verify().await, sunbeam_sdk::manifests::cmd_apply(&env_str, &domain, &email, &ns).await
}
Some(Verb::Logs { target, follow }) => { PlatformAction::Seed => sunbeam_sdk::secrets::cmd_seed().await,
sunbeam_sdk::services::cmd_logs(&target, follow).await
}
Some(Verb::Get { target, output }) => { PlatformAction::Verify => sunbeam_sdk::secrets::cmd_verify().await,
sunbeam_sdk::services::cmd_get(&target, &output).await
}
Some(Verb::Restart { target }) => { PlatformAction::Logs { target, follow } => {
sunbeam_sdk::services::cmd_restart(target.as_deref()).await sunbeam_sdk::services::cmd_logs(&target, follow).await
} }
Some(Verb::Build { what, push, deploy, no_cache }) => { PlatformAction::Get { target, output } => {
let push = push || deploy; sunbeam_sdk::services::cmd_get(&target, &output).await
sunbeam_sdk::images::cmd_build(&what, push, deploy, no_cache).await }
}
Some(Verb::Check { target }) => { PlatformAction::Restart { target } => {
sunbeam_sdk::checks::cmd_check(target.as_deref()).await sunbeam_sdk::services::cmd_restart(target.as_deref()).await
} }
Some(Verb::Mirror) => sunbeam_sdk::images::cmd_mirror().await, PlatformAction::Build { what, push, deploy, no_cache } => {
let push = push || deploy;
sunbeam_sdk::images::cmd_build(&what, push, deploy, no_cache).await
}
Some(Verb::Bootstrap) => sunbeam_sdk::gitea::cmd_bootstrap().await, PlatformAction::Check { target } => {
sunbeam_sdk::checks::cmd_check(target.as_deref()).await
}
PlatformAction::Mirror => sunbeam_sdk::images::cmd_mirror().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 => {
@@ -908,14 +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::Bao { bao_args }) => {
sunbeam_sdk::kube::cmd_bao(&bao_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(),
@@ -972,20 +934,6 @@ pub async fn dispatch() -> Result<()> {
sunbeam_sdk::openbao::cli::dispatch(action, &sc, cli.output_format).await sunbeam_sdk::openbao::cli::dispatch(action, &sc, cli.output_format).await
} }
Some(Verb::People { action }) => {
let sc = sunbeam_sdk::client::SunbeamClient::from_context(
&sunbeam_sdk::config::active_context(),
);
sunbeam_sdk::lasuite::cli::dispatch_people(action, &sc, cli.output_format).await
}
Some(Verb::Docs { action }) => {
let sc = sunbeam_sdk::client::SunbeamClient::from_context(
&sunbeam_sdk::config::active_context(),
);
sunbeam_sdk::lasuite::cli::dispatch_docs(action, &sc, cli.output_format).await
}
Some(Verb::Meet { action }) => { Some(Verb::Meet { 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(),