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