feat/context-openbao-url #339
@@ -15,6 +15,7 @@ use anyhow::{Context, Result, bail};
|
||||
use harmony::topology::{K8sAnywhereConfig, K8sAnywhereTopology};
|
||||
use harmony_config::{Config, ConfigClient};
|
||||
use harmony_k8s::K8sClient;
|
||||
use log::{debug, info};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tempfile::NamedTempFile;
|
||||
@@ -39,6 +40,8 @@ struct ContextDef {
|
||||
autoprovision: bool,
|
||||
k3d: Option<String>,
|
||||
openbao_namespace: Option<String>,
|
||||
openbao_url: Option<String>,
|
||||
zitadel_url: Option<String>,
|
||||
}
|
||||
|
||||
/// The cluster K8sAnywhereTopology autoprovisions (its K3DInstallationScore
|
||||
@@ -100,20 +103,37 @@ impl AppContext {
|
||||
)
|
||||
})?;
|
||||
|
||||
info!(
|
||||
"Resolving deploy context '{name}' (profile {:?})",
|
||||
def.profile
|
||||
);
|
||||
debug!("Context '{name}' definition: {def:?}");
|
||||
let guard = match (def.autoprovision, &def.k3d, &def.openbao_namespace) {
|
||||
(true, None, None) => None,
|
||||
(false, Some(cluster), None) => Some(k3d_kubeconfig(cluster)?),
|
||||
(true, None, None) => {
|
||||
info!("Cluster access: autoprovision local k3d ('{AUTOPROVISION_CLUSTER}')");
|
||||
None
|
||||
}
|
||||
(false, Some(cluster), None) => {
|
||||
info!("Cluster access: existing local k3d cluster '{cluster}'");
|
||||
Some(k3d_kubeconfig(cluster)?)
|
||||
}
|
||||
(false, None, Some(ns)) => {
|
||||
let access: ClusterAccess = ConfigClient::for_namespace(ns)
|
||||
.await
|
||||
.get()
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading cluster kubeconfig from OpenBao ({ns}/ClusterAccess) \
|
||||
— are the Zitadel key + OPENBAO_* env vars set?)"
|
||||
)
|
||||
})?;
|
||||
info!("Cluster access: kubeconfig from OpenBao (namespace '{ns}')");
|
||||
// The kubeconfig is an OpenBao secret, so read it from an
|
||||
// OpenBao-only client built with the context's URLs (each
|
||||
// falling through to env when unset).
|
||||
let access: ClusterAccess =
|
||||
openbao_client(ns, def.openbao_url.clone(), def.zitadel_url.clone())
|
||||
.await
|
||||
.with_context(|| format!("reaching OpenBao for context '{name}'"))?
|
||||
.get()
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"loading cluster kubeconfig from OpenBao ({ns}/ClusterAccess) \
|
||||
— are the Zitadel key + OpenBao URL/role set?"
|
||||
)
|
||||
})?;
|
||||
Some(write_kubeconfig(access.kubeconfig.as_bytes())?)
|
||||
}
|
||||
_ => bail!(
|
||||
@@ -211,6 +231,24 @@ fn find_contexts_file() -> Result<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// An OpenBao-only `ConfigClient` for `namespace`, using the given URLs (each
|
||||
/// falling through to env). Errors when no OpenBao endpoint is resolvable.
|
||||
async fn openbao_client(
|
||||
namespace: &str,
|
||||
openbao_url: Option<String>,
|
||||
zitadel_url: Option<String>,
|
||||
) -> Result<ConfigClient> {
|
||||
let source = harmony_config::openbao_source(namespace, openbao_url, zitadel_url)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"no OpenBao source for '{namespace}' — set `openbao_url`/OPENBAO_URL \
|
||||
and the Zitadel auth env (HARMONY_ZITADEL_KEY_*, AUDIENCE, OPENBAO_JWT_ROLE)"
|
||||
)
|
||||
})?;
|
||||
Ok(ConfigClient::new(vec![source]))
|
||||
}
|
||||
|
||||
fn write_kubeconfig(contents: &[u8]) -> Result<NamedTempFile> {
|
||||
let mut file =
|
||||
NamedTempFile::with_prefix("harmony-ctx-").context("create kubeconfig tempfile")?;
|
||||
|
||||
@@ -196,31 +196,43 @@ impl ConfigClient {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build an OpenBao-backed `StoreSource` from env vars, or `None` (with a
|
||||
/// `warn!`) when `OPENBAO_URL`/`VAULT_ADDR` is unset or the connection
|
||||
/// fails — so a missing OpenBao degrades the chain instead of breaking
|
||||
/// startup.
|
||||
/// Build an OpenBao-backed `StoreSource` purely from env — the default chain.
|
||||
async fn openbao_from_env(namespace: &str) -> Option<Arc<dyn ConfigSource>> {
|
||||
let Some(url) = std::env::var("OPENBAO_URL")
|
||||
.or_else(|_| std::env::var("VAULT_ADDR"))
|
||||
.ok()
|
||||
openbao_source(namespace, None, None).await
|
||||
}
|
||||
|
||||
/// Build an OpenBao-backed `StoreSource`. `openbao_url` and `zitadel_sso_url`
|
||||
/// override their env vars (`OPENBAO_URL`/`VAULT_ADDR`, `HARMONY_SSO_URL`) when
|
||||
/// `Some`, else fall through to the env — so a caller that knows the endpoints
|
||||
/// (e.g. a deploy context) supplies them without mutating process env. Returns
|
||||
/// `None` (with a `warn!`) when no OpenBao URL is resolvable or the connection
|
||||
/// fails, so a missing OpenBao degrades the chain instead of breaking startup.
|
||||
pub async fn openbao_source(
|
||||
namespace: &str,
|
||||
openbao_url: Option<String>,
|
||||
zitadel_sso_url: Option<String>,
|
||||
) -> Option<Arc<dyn ConfigSource>> {
|
||||
let env = |k: &str| std::env::var(k).ok();
|
||||
|
||||
let Some(url) = openbao_url
|
||||
.or_else(|| env("OPENBAO_URL"))
|
||||
.or_else(|| env("VAULT_ADDR"))
|
||||
else {
|
||||
warn!("OpenBao URL not set; OpenBao source omitted from chain");
|
||||
return None;
|
||||
};
|
||||
|
||||
let env = |k: &str| std::env::var(k).ok();
|
||||
// The Zitadel base for both the JWT-bearer issuer and the OIDC rung.
|
||||
let sso_url = zitadel_sso_url.or_else(|| env("HARMONY_SSO_URL"));
|
||||
|
||||
// Headless Zitadel-machine → OpenBao (JWT-bearer) rung: needs a machine
|
||||
// keyfile (path or inline JSON) plus the project-ID audience. The issuer
|
||||
// is the same Zitadel base the OIDC rung uses (`HARMONY_SSO_URL`). Absent
|
||||
// any piece, the rung stays off and the ladder falls through.
|
||||
// keyfile (path or inline JSON) plus the project-ID audience. Absent any
|
||||
// piece, the rung stays off and the ladder falls through.
|
||||
let zitadel_jwt_bearer = {
|
||||
let key_path = env("HARMONY_ZITADEL_KEY_PATH").map(PathBuf::from);
|
||||
let key_json = env("HARMONY_ZITADEL_KEY_JSON");
|
||||
match (
|
||||
key_path.is_some() || key_json.is_some(),
|
||||
env("HARMONY_SSO_URL"),
|
||||
sso_url.clone(),
|
||||
env("HARMONY_ZITADEL_AUDIENCE"),
|
||||
) {
|
||||
(true, Some(oidc_issuer_url), Some(audience)) => {
|
||||
@@ -243,7 +255,7 @@ async fn openbao_from_env(namespace: &str) -> Option<Arc<dyn ConfigSource>> {
|
||||
token: env("OPENBAO_TOKEN"),
|
||||
username: env("OPENBAO_USERNAME"),
|
||||
password: env("OPENBAO_PASSWORD"),
|
||||
zitadel_sso_url: env("HARMONY_SSO_URL"),
|
||||
zitadel_sso_url: sso_url,
|
||||
zitadel_client_id: env("HARMONY_SSO_CLIENT_ID"),
|
||||
jwt_role: env("OPENBAO_JWT_ROLE"),
|
||||
jwt_auth_mount: env("OPENBAO_JWT_AUTH_MOUNT"),
|
||||
|
||||
Reference in New Issue
Block a user