From 13e3f5d42e6eb7dbc11dfb75cf323c8fdc62e903 Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Mon, 23 Mar 2026 08:48:33 +0000 Subject: [PATCH] fix opensearch pod resolution + sol-agent vault policy os_api: resolve pod name by label instead of hardcoded opensearch-0. added find_pod_by_label helper to kube.rs. secrets.py: sol-agent policy (read/write sol-tokens/*) and k8s auth role bound to matrix namespace default SA. --- src/kube.rs | 19 +++++++++++++++++++ src/manifests.rs | 12 +++++++++--- sunbeam-sdk/src/kube/mod.rs | 19 +++++++++++++++++++ sunbeam-sdk/src/manifests/mod.rs | 11 ++++++++--- sunbeam/secrets.py | 25 +++++++++++++++++++++++++ 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/kube.rs b/src/kube.rs index a6b10f5..2de6d25 100644 --- a/src/kube.rs +++ b/src/kube.rs @@ -305,6 +305,25 @@ pub async fn create_secret(ns: &str, name: &str, data: HashMap) Ok(()) } +/// Find the first Running pod matching a label selector in a namespace. +pub async fn find_pod_by_label(ns: &str, label: &str) -> Option { + let client = get_client().await.ok()?; + let pods: kube::Api = + kube::Api::namespaced(client, ns); + let lp = kube::api::ListParams::default().labels(label); + let pod_list = pods.list(&lp).await.ok()?; + pod_list + .items + .iter() + .find(|p| { + p.status + .as_ref() + .and_then(|s| s.phase.as_deref()) + == Some("Running") + }) + .and_then(|p| p.metadata.name.clone()) +} + /// Execute a command in a pod and return (exit_code, stdout). #[allow(dead_code)] pub async fn kube_exec( diff --git a/src/manifests.rs b/src/manifests.rs index f0761b9..d45bfbe 100644 --- a/src/manifests.rs +++ b/src/manifests.rs @@ -475,10 +475,16 @@ async fn os_api(path: &str, method: &str, body: Option<&str>) -> Option curl_args.extend_from_slice(&["-H", "Content-Type: application/json", "-d", &body_string]); } - // Build the full exec command: exec deploy/opensearch -n data -c opensearch -- curl ... - let exec_cmd = curl_args; + // Resolve the actual pod name from the app=opensearch label + let pod_name = match crate::kube::find_pod_by_label("data", "app=opensearch").await { + Some(name) => name, + None => { + crate::output::warn("No OpenSearch pod found in data namespace"); + return None; + } + }; - match crate::kube::kube_exec("data", "opensearch-0", &exec_cmd, Some("opensearch")).await { + match crate::kube::kube_exec("data", &pod_name, &curl_args, Some("opensearch")).await { Ok((0, out)) if !out.is_empty() => Some(out), _ => None, } diff --git a/sunbeam-sdk/src/kube/mod.rs b/sunbeam-sdk/src/kube/mod.rs index 70bcea7..c0cdde6 100644 --- a/sunbeam-sdk/src/kube/mod.rs +++ b/sunbeam-sdk/src/kube/mod.rs @@ -308,6 +308,25 @@ pub async fn create_secret(ns: &str, name: &str, data: HashMap) Ok(()) } +/// Find the first Running pod matching a label selector in a namespace. +pub async fn find_pod_by_label(ns: &str, label: &str) -> Option { + let client = get_client().await.ok()?; + let pods: kube::Api = + kube::Api::namespaced(client.clone(), ns); + let lp = kube::api::ListParams::default().labels(label); + let pod_list = pods.list(&lp).await.ok()?; + pod_list + .items + .iter() + .find(|p| { + p.status + .as_ref() + .and_then(|s| s.phase.as_deref()) + == Some("Running") + }) + .and_then(|p| p.metadata.name.clone()) +} + /// Execute a command in a pod and return (exit_code, stdout). #[allow(dead_code)] pub async fn kube_exec( diff --git a/sunbeam-sdk/src/manifests/mod.rs b/sunbeam-sdk/src/manifests/mod.rs index f0761b9..e40bf23 100644 --- a/sunbeam-sdk/src/manifests/mod.rs +++ b/sunbeam-sdk/src/manifests/mod.rs @@ -475,10 +475,15 @@ async fn os_api(path: &str, method: &str, body: Option<&str>) -> Option curl_args.extend_from_slice(&["-H", "Content-Type: application/json", "-d", &body_string]); } - // Build the full exec command: exec deploy/opensearch -n data -c opensearch -- curl ... - let exec_cmd = curl_args; + let pod_name = match crate::kube::find_pod_by_label("data", "app=opensearch").await { + Some(name) => name, + None => { + crate::output::warn("No OpenSearch pod found in data namespace"); + return None; + } + }; - match crate::kube::kube_exec("data", "opensearch-0", &exec_cmd, Some("opensearch")).await { + match crate::kube::kube_exec("data", &pod_name, &curl_args, Some("opensearch")).await { Ok((0, out)) if !out.is_empty() => Some(out), _ => None, } diff --git a/sunbeam/secrets.py b/sunbeam/secrets.py index 124ea73..b28c0d3 100644 --- a/sunbeam/secrets.py +++ b/sunbeam/secrets.py @@ -383,6 +383,14 @@ def _seed_openbao() -> dict: "turn-secret": tuwunel["turn-secret"], "registration-token": tuwunel["registration-token"]}) + # Patch gitea admin credentials into secret/sol for Sol's Gitea integration. + # Uses kv patch (not put) to preserve manually-set keys (matrix-access-token etc.). + ok("Patching Gitea admin credentials into secret/sol...") + bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' " + f"bao kv patch secret/sol " + f"gitea-admin-username='{gitea['admin-username']}' " + f"gitea-admin-password='{gitea['admin-password']}'") + # Configure Kubernetes auth method so VSO can authenticate with OpenBao ok("Configuring Kubernetes auth for VSO...") bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' " @@ -407,6 +415,23 @@ def _seed_openbao() -> dict: f"policies=vso-reader " f"ttl=1h") + # Sol agent policy — read/write access to sol-tokens/* for user impersonation PATs + ok("Configuring Kubernetes auth for Sol agent...") + sol_policy_hcl = ( + 'path "secret/data/sol-tokens/*" { capabilities = ["create", "read", "update", "delete"] }\n' + 'path "secret/metadata/sol-tokens/*" { capabilities = ["read", "delete", "list"] }\n' + ) + sol_policy_b64 = base64.b64encode(sol_policy_hcl.encode()).decode() + bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' " + f"sh -c 'echo {sol_policy_b64} | base64 -d | bao policy write sol-agent -'") + + bao(f"BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN='{root_token}' " + f"bao write auth/kubernetes/role/sol-agent " + f"bound_service_account_names=default " + f"bound_service_account_namespaces=matrix " + f"policies=sol-agent " + f"ttl=1h") + return { "hydra-system-secret": hydra["system-secret"], "hydra-cookie-secret": hydra["cookie-secret"],