From 24e98b4e7db41a18522f7961799b95359369140c Mon Sep 17 00:00:00 2001 From: Sienna Meridian Satterwhite Date: Fri, 20 Mar 2026 13:29:59 +0000 Subject: [PATCH] fix: CNPG readiness, DKIM SPKI format, kv_patch, container name - Check CNPG Cluster CRD status.phase instead of pod Running phase - DKIM public key: use SPKI format (BEGIN PUBLIC KEY) matching Python - Use kv_patch instead of kv_put for dirty paths (preserves external fields) - Vault KV only written when password is newly generated - Gitea exec passes container name Some("gitea") - Fix openbao comment (400 not 409) --- src/openbao.rs | 2 +- src/secrets.rs | 69 +++++++++++++++++++++++++++++++------------------- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/openbao.rs b/src/openbao.rs index 729665e..c71951c 100644 --- a/src/openbao.rs +++ b/src/openbao.rs @@ -158,7 +158,7 @@ impl BaoClient { // ── Secrets engine management ─────────────────────────────────────── /// Enable a secrets engine at the given path. - /// Returns Ok(()) even if already enabled (409 is tolerated). + /// Returns Ok(()) even if already enabled (400 is tolerated). pub async fn enable_secrets_engine(&self, path: &str, engine_type: &str) -> Result<()> { #[derive(Serialize)] struct EnableRequest<'a> { diff --git a/src/secrets.rs b/src/secrets.rs index cfc8722..47904f6 100644 --- a/src/secrets.rs +++ b/src/secrets.rs @@ -6,10 +6,9 @@ use crate::error::{Result, ResultExt, SunbeamError}; use k8s_openapi::api::core::v1::Pod; -use kube::api::{Api, ListParams}; +use kube::api::{Api, ApiResource, DynamicObject, ListParams}; use rand::RngCore; -use rsa::pkcs1::EncodeRsaPublicKey; -use rsa::pkcs8::EncodePrivateKey; +use rsa::pkcs8::{EncodePrivateKey, EncodePublicKey}; use rsa::RsaPrivateKey; use serde::Deserialize; use std::collections::{HashMap, HashSet}; @@ -73,7 +72,7 @@ fn gen_dkim_key_pair() -> (String, String) { }; let public_key = private_key.to_public_key(); - let public_pem = match public_key.to_pkcs1_pem(rsa::pkcs1::LineEnding::LF) { + let public_pem = match public_key.to_public_key_pem(rsa::pkcs8::LineEnding::LF) { Ok(p) => p.to_string(), Err(e) => { warn(&format!("Public key PEM encoding failed: {e}")); @@ -643,7 +642,7 @@ async fn seed_openbao() -> Result> { for (path, data) in all_paths { if dirty_paths.contains(*path) { - bao.kv_put("secret", path, data).await?; + bao.kv_patch("secret", path, data).await?; } } } @@ -739,16 +738,17 @@ async fn configure_db_engine(bao: &BaoClient) -> Result<()> { let existing_vault_pass = bao.kv_get_field("secret", "vault", "pg-password").await?; let vault_pg_pass = if existing_vault_pass.is_empty() { - rand_token() + let new_pass = rand_token(); + let mut vault_data = HashMap::new(); + vault_data.insert("pg-password".to_string(), new_pass.clone()); + bao.kv_put("secret", "vault", &vault_data).await?; + ok("vault KV entry written."); + new_pass } else { + ok("vault KV entry already present -- skipping write."); existing_vault_pass }; - let mut vault_data = HashMap::new(); - vault_data.insert("pg-password".to_string(), vault_pg_pass.clone()); - bao.kv_put("secret", "vault", &vault_data).await?; - ok("vault KV entry written."); - let create_vault_sql = concat!( "DO $$ BEGIN ", "IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'vault') THEN ", @@ -981,18 +981,31 @@ pub async fn cmd_seed() -> Result<()> { let mut pg_pod = String::new(); let client = k::get_client().await?; + let ar = ApiResource { + group: "postgresql.cnpg.io".into(), + version: "v1".into(), + api_version: "postgresql.cnpg.io/v1".into(), + kind: "Cluster".into(), + plural: "clusters".into(), + }; + let cnpg_api: Api = Api::namespaced_with(client.clone(), "data", &ar); for _ in 0..60 { - let pods: Api = Api::namespaced(client.clone(), "data"); - let lp = ListParams::default().labels("cnpg.io/cluster=postgres,role=primary"); - if let Ok(pod_list) = pods.list(&lp).await { - if let Some(pod) = pod_list.items.first() { - if let Some(name) = pod.metadata.name.as_deref() { - if pod - .status - .as_ref() - .and_then(|s| s.phase.as_deref()) - .unwrap_or("") - == "Running" + if let Ok(cluster) = cnpg_api.get("postgres").await { + let phase = cluster + .data + .get("status") + .and_then(|s| s.get("phase")) + .and_then(|p| p.as_str()) + .unwrap_or(""); + if phase == "Cluster in healthy state" { + // Cluster is healthy — find the primary pod name + let pods: Api = Api::namespaced(client.clone(), "data"); + let lp = ListParams::default().labels("cnpg.io/cluster=postgres,role=primary"); + if let Ok(pod_list) = pods.list(&lp).await { + if let Some(name) = pod_list + .items + .first() + .and_then(|p| p.metadata.name.as_deref()) { pg_pod = name.to_string(); ok(&format!("Postgres ready ({pg_pod}).")); @@ -1137,7 +1150,7 @@ pub async fn cmd_seed() -> Result<()> { &gitea_admin_pass, "--must-change-password=false", ], - None, + Some("gitea"), ) .await { @@ -1517,8 +1530,12 @@ mod tests { "Private key should be PKCS8 PEM" ); assert!( - public_pem.contains("BEGIN RSA PUBLIC KEY"), - "Public key should be PEM" + public_pem.contains("BEGIN PUBLIC KEY"), + "Public key should be SPKI PEM (not PKCS#1)" + ); + assert!( + !public_pem.contains("BEGIN RSA PUBLIC KEY"), + "Public key should NOT be PKCS#1 format" ); assert!(!private_pem.is_empty()); assert!(!public_pem.is_empty()); @@ -1576,7 +1593,7 @@ mod tests { #[test] fn test_dkim_public_key_extraction() { - let pem = "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQ...\nbase64data\n-----END RSA PUBLIC KEY-----"; + let pem = "-----BEGIN PUBLIC KEY-----\nMIIBCgKCAQ...\nbase64data\n-----END PUBLIC KEY-----"; let b64_key: String = pem .replace("-----BEGIN PUBLIC KEY-----", "") .replace("-----END PUBLIC KEY-----", "")