refactor(org-ops): neutralize BXNet identity from framework core
Move all BXNet-specific defaults out of org-ops-core into the example CLI binary (org-ops-cli/src/main.rs). The framework is now fork-ready for any consortium without string-replacing org-specific values. Changes: - OrgOpsConfig: neutral defaults (my-org, example.com), added infra_namespace, bridge_daemonset, ssh_user fields - AuthConfig: config_dir_name field replaces hardcoded bxnet-ops path; config_dir() now a method using config value - GitConfig: neutral default (git.example.com) - auth_commands: DID_BRIDGE_PATH env replaces hardcoded dev path - lib.rs (connect): namespace, daemonset, SSH user, cert paths all read from OrgOpsConfig instead of hardcoded strings - score_fetcher: neutral corpus entry name - playbook_commands: neutral Keycloak service name and temp dir - org-ops-cli/main.rs: explicit BXNet example with "replace these" comment, all org-specific values passed via config - .gitignore: added target/ Zero BXNet references remain in org-ops-core source (verified by grep). Signed-off-by: Tyler King <tking@guildhouse.dev> Signed-off-by: Tyler J King <tking727@gmail.com>
This commit is contained in:
parent
b6d9b7fa97
commit
c68456d745
8 changed files with 105 additions and 58 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
target/
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Option<std::process::Child>> {
|
||||
// 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)?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue