diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e676f74 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/org-ops-cli/src/main.rs b/org-ops-cli/src/main.rs index 148833b..1b0050b 100644 --- a/org-ops-cli/src/main.rs +++ b/org-ops-cli/src/main.rs @@ -1,3 +1,9 @@ +//! Example org-ops CLI — BXNet consortium. +//! +//! This binary demonstrates how to build a governed CLI for your +//! consortium using the org-ops-core framework. Fork this file and +//! replace the config values with your org's identity. + use org_ops_core::{ AuthCommands, AuthConfig, GitConfig, GovernedGitCommands, OrgOps, OrgOpsConfig, PlaybookCommands, @@ -8,6 +14,7 @@ fn main() -> anyhow::Result<()> { OrgOps::builder() .with_config(OrgOpsConfig { + // ── Replace these with your consortium's values ── org_name: "BXNet".into(), trust_domain: "bxnet.io".into(), bascule_endpoint: "bascule.bxnet.io:443".into(), @@ -15,8 +22,16 @@ fn main() -> anyhow::Result<()> { binary_name: "bxnet-ops".into(), description: "BXNet governed operations CLI".into(), version: env!("CARGO_PKG_VERSION").into(), + infra_namespace: "guildhouse-infra".into(), + bridge_daemonset: "substrate-bridge".into(), + ssh_user: "tking".into(), }) - .with_commands(AuthCommands::new(AuthConfig::default())) + .with_commands(AuthCommands::new(AuthConfig { + oidc_issuer: "https://auth.bxnet.io/realms/guildhouse".into(), + client_id: "bxnet-ops".into(), + config_dir_name: "bxnet-ops".into(), + ..Default::default() + })) .with_commands(GovernedGitCommands::new(GitConfig { forgejo_url: "https://git.bxnet.io".into(), forgejo_token, diff --git a/org-ops-core/src/auth_commands.rs b/org-ops-core/src/auth_commands.rs index fce37df..b21f25b 100644 --- a/org-ops-core/src/auth_commands.rs +++ b/org-ops-core/src/auth_commands.rs @@ -14,37 +14,24 @@ pub struct AuthConfig { pub oidc_issuer: String, pub client_id: String, pub did_bridge_port: u16, + /// Directory name under ~/.config/ for credential storage. + pub config_dir_name: String, } impl Default for AuthConfig { fn default() -> Self { Self { - oidc_issuer: "https://auth.bxnet.io/realms/guildhouse".into(), - client_id: "bxnet-ops".into(), + oidc_issuer: "https://auth.example.com/realms/consortium".into(), + client_id: "org-ops".into(), did_bridge_port: 7777, + config_dir_name: "org-ops".into(), } } } -fn config_dir() -> PathBuf { +fn config_dir_for(name: &str) -> PathBuf { let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); - PathBuf::from(home).join(".config").join("bxnet-ops") -} - -fn cert_path() -> PathBuf { - config_dir().join("identity.pem") -} - -fn key_path() -> PathBuf { - config_dir().join("identity.key") -} - -fn did_path() -> PathBuf { - config_dir().join("identity.did") -} - -fn expiry_path() -> PathBuf { - config_dir().join("identity.expiry") + PathBuf::from(home).join(".config").join(name) } pub struct AuthCommands { @@ -56,8 +43,28 @@ impl AuthCommands { Self { config } } - fn ensure_config_dir() { - let dir = config_dir(); + fn config_dir(&self) -> PathBuf { + config_dir_for(&self.config.config_dir_name) + } + + fn cert_path(&self) -> PathBuf { + self.config_dir().join("identity.pem") + } + + fn key_path(&self) -> PathBuf { + self.config_dir().join("identity.key") + } + + fn did_path(&self) -> PathBuf { + self.config_dir().join("identity.did") + } + + fn expiry_path(&self) -> PathBuf { + self.config_dir().join("identity.expiry") + } + + fn ensure_config_dir(&self) { + let dir = self.config_dir(); if !dir.exists() { fs::create_dir_all(&dir).ok(); #[cfg(unix)] @@ -69,10 +76,9 @@ impl AuthCommands { } fn start_did_bridge(&self) -> anyhow::Result> { - // Find did_bridge.py - let home = std::env::var("HOME").unwrap_or_default(); + // Find did_bridge.py — check DID_BRIDGE_PATH env, then cwd let bridge_paths = [ - format!("{}/projects/substrate-project/guildhouse/services/did-bridge/did_bridge.py", home), + std::env::var("DID_BRIDGE_PATH").unwrap_or_default(), "did_bridge.py".to_string(), ]; @@ -134,7 +140,7 @@ impl AuthCommands { println!("Authenticating via OIDC..."); println!(" Issuer: {}", self.config.oidc_issuer); - Self::ensure_config_dir(); + self.ensure_config_dir(); // Start did-bridge eprintln!("Starting did-bridge..."); @@ -229,21 +235,21 @@ impl AuthCommands { let (cert, key, did, expires) = result?; // Store certificate and key (not token) - fs::write(cert_path(), &cert)?; - fs::write(key_path(), &key)?; - fs::write(did_path(), &did)?; - fs::write(expiry_path(), expires.to_string())?; + fs::write(self.cert_path(), &cert)?; + fs::write(self.key_path(), &key)?; + fs::write(self.did_path(), &did)?; + fs::write(self.expiry_path(), expires.to_string())?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; - fs::set_permissions(key_path(), fs::Permissions::from_mode(0o600)).ok(); + fs::set_permissions(self.key_path(), fs::Permissions::from_mode(0o600)).ok(); } println!(); println!("Authenticated."); println!(" DID: {}", did); - println!(" Certificate: {}", cert_path().display()); + println!(" Certificate: {}", self.cert_path().display()); println!(" Expires: 1 hour"); println!(" Token: zeroized"); @@ -251,14 +257,14 @@ impl AuthCommands { } fn cmd_status(&self, _ctx: &SessionContext) -> anyhow::Result<()> { - if !cert_path().exists() { + if !self.cert_path().exists() { println!("Not authenticated."); - println!("Run: guildhouse-ops auth login"); + println!("Run: auth login"); return Ok(()); } - let did = fs::read_to_string(did_path()).unwrap_or("unknown".into()); - let expires: i64 = fs::read_to_string(expiry_path()) + let did = fs::read_to_string(self.did_path()).unwrap_or("unknown".into()); + let expires: i64 = fs::read_to_string(self.expiry_path()) .unwrap_or("0".into()) .trim() .parse() @@ -271,7 +277,7 @@ impl AuthCommands { if expires > 0 && now > expires { println!("Session expired."); - println!("Run: guildhouse-ops auth login"); + println!("Run: auth login"); return Ok(()); } @@ -284,13 +290,13 @@ impl AuthCommands { println!("Authenticated"); println!(" DID: {}", did); println!(" Remaining: {}", remaining); - println!(" Certificate: {}", cert_path().display()); + println!(" Certificate: {}", self.cert_path().display()); Ok(()) } fn cmd_logout(&self, _ctx: &SessionContext) -> anyhow::Result<()> { - for path in &[cert_path(), key_path(), did_path(), expiry_path()] { + for path in &[self.cert_path(), self.key_path(), self.did_path(), self.expiry_path()] { if path.exists() { fs::remove_file(path)?; } diff --git a/org-ops-core/src/config.rs b/org-ops-core/src/config.rs index a704cc9..6606bdd 100644 --- a/org-ops-core/src/config.rs +++ b/org-ops-core/src/config.rs @@ -1,5 +1,16 @@ /// Configuration for an org-ops instance. -/// Fork org-ops and set these values for your consortium. +/// +/// Set these values for your consortium in the CLI binary's `main()`: +/// ```ignore +/// OrgOps::builder() +/// .with_config(OrgOpsConfig { +/// org_name: "MyOrg".into(), +/// trust_domain: "myorg.example.com".into(), +/// ..Default::default() +/// }) +/// .build() +/// .run() +/// ``` #[derive(Debug, Clone)] pub struct OrgOpsConfig { pub org_name: String, @@ -9,18 +20,27 @@ pub struct OrgOpsConfig { pub binary_name: String, pub description: String, pub version: String, + /// Kubernetes namespace for port-forward and corpus lookups. + pub infra_namespace: String, + /// DaemonSet name for Bascule port-forward target. + pub bridge_daemonset: String, + /// SSH user for governed shell connections. + pub ssh_user: String, } impl Default for OrgOpsConfig { fn default() -> Self { Self { - org_name: "BXNet".into(), - trust_domain: "bxnet.io".into(), - bascule_endpoint: "bascule.bxnet.io:443".into(), - chronicle_endpoint: "chronicle.bxnet.io:8080".into(), - binary_name: "bxnet-ops".into(), - description: "BXNet governed operations CLI".into(), + org_name: "my-org".into(), + trust_domain: "example.com".into(), + bascule_endpoint: "localhost:2222".into(), + chronicle_endpoint: "localhost:8090".into(), + binary_name: "org-ops".into(), + description: "Governed operations CLI".into(), version: env!("CARGO_PKG_VERSION").into(), + infra_namespace: "guildhouse-infra".into(), + bridge_daemonset: "substrate-bridge".into(), + ssh_user: std::env::var("USER").unwrap_or_else(|_| "operator".into()), } } } diff --git a/org-ops-core/src/git_commands.rs b/org-ops-core/src/git_commands.rs index caf5c30..f66543e 100644 --- a/org-ops-core/src/git_commands.rs +++ b/org-ops-core/src/git_commands.rs @@ -28,9 +28,9 @@ pub struct GitConfig { impl Default for GitConfig { fn default() -> Self { Self { - forgejo_url: "https://git.bxnet.io".into(), + forgejo_url: "https://git.example.com".into(), forgejo_token: None, - chronicle_webhook: "http://localhost:8090/webhook/forgejo".into(), + chronicle_webhook: "http://localhost:8090/webhook/cloudevents".into(), } } } diff --git a/org-ops-core/src/lib.rs b/org-ops-core/src/lib.rs index 992aad1..a3fb40a 100644 --- a/org-ops-core/src/lib.rs +++ b/org-ops-core/src/lib.rs @@ -192,8 +192,8 @@ impl OrgOps { let pf = std::process::Command::new("kubectl") .args([ "port-forward", - "-n", "guildhouse-infra", - "daemonset/substrate-bridge", + "-n", &self.config.infra_namespace, + &format!("daemonset/{}", self.config.bridge_daemonset), "12222:2222", ]) .stdout(std::process::Stdio::null()) @@ -219,8 +219,9 @@ impl OrgOps { // 5. Exec SSH — use certificate if available let home = std::env::var("HOME").unwrap_or_default(); - let cert_file = format!("{}/.config/guildhouse-ops/identity.pem", home); - let key_file = format!("{}/.config/guildhouse-ops/identity.key", home); + let config_name = &self.config.binary_name; + let cert_file = format!("{}/.config/{}/identity.pem", home, config_name); + let key_file = format!("{}/.config/{}/identity.key", home, config_name); let has_cert = std::path::Path::new(&cert_file).exists() && std::path::Path::new(&key_file).exists(); @@ -240,7 +241,7 @@ impl OrgOps { eprintln!(" Tip: run 'auth login' for certificate auth"); } - ssh_args.push(format!("tking@{ssh_host}")); + ssh_args.push(format!("{}@{ssh_host}", self.config.ssh_user)); let status = std::process::Command::new("ssh") .args(&ssh_args) diff --git a/org-ops-core/src/playbook_commands.rs b/org-ops-core/src/playbook_commands.rs index 9bc2038..929d928 100644 --- a/org-ops-core/src/playbook_commands.rs +++ b/org-ops-core/src/playbook_commands.rs @@ -152,12 +152,12 @@ impl PlaybookCommands { let gsap_ac = if let Ok(broker_url) = std::env::var("GSAP_BROKER_URL") { let token = std::env::var("GSAP_BEARER_TOKEN").unwrap_or_default(); let driver_id = std::env::var("GSAP_DRIVER_ID") - .unwrap_or_else(|_| "keycloak-guildhouse".into()); + .unwrap_or_else(|_| "keycloak".into()); let accord = std::env::var("GSAP_ACCORD_TEMPLATE") .unwrap_or_else(|_| accord_name.clone()); let session_dir = std::env::var("GSAP_SESSION_DIR") .map(PathBuf::from) - .unwrap_or_else(|_| std::env::temp_dir().join("bxnet-gsap")); + .unwrap_or_else(|_| std::env::temp_dir().join("org-ops-gsap")); let client = GsapClient::new(broker_url, token, session_dir); let params_json = serde_json::to_string(&extra_vars).unwrap_or_default(); diff --git a/org-ops-core/src/score_fetcher.rs b/org-ops-core/src/score_fetcher.rs index 3085782..571db94 100644 --- a/org-ops-core/src/score_fetcher.rs +++ b/org-ops-core/src/score_fetcher.rs @@ -45,7 +45,11 @@ pub fn fetch_score(entry_name: &str) -> WorkloadRiskScore { } } -/// Fetch the cluster's best Tier A corpus entry score. +/// Fetch the cluster's default corpus entry score. +/// +/// Callers should pass the org's binary name or a configured corpus +/// entry name rather than hardcoding an org-specific value. pub fn fetch_cluster_score() -> WorkloadRiskScore { - fetch_score("bxnet-ops") + // Default corpus entry — overridden by orgs in their CLI binary. + fetch_score("org-ops") }