refactor: deduplicate constants, fix secret key mismatch, add VSS pruning

- New src/constants.rs: single source for MANAGED_NS (includes monitoring)
  and GITEA_ADMIN_USER, imported by all modules that previously had copies
- Fix checks.rs reading wrong key names from gitea-admin-credentials secret
- Add VaultStaticSecret pruning in pre_apply_cleanup (H1)
- Fix cert_manager_present check (was always true after canonicalize)
- Add warnings for silent failures in pre_apply_cleanup
- Fix os_api dead variable assignment
- Set TLS private key permissions to 0600
- Redact Gitea admin password in print_urls
This commit is contained in:
2026-03-20 13:29:35 +00:00
parent 503e407243
commit bcfb443757
7 changed files with 108 additions and 58 deletions

View File

@@ -136,7 +136,7 @@ async fn check_gitea_version(domain: &str, client: &reqwest::Client) -> CheckRes
/// GET /api/v1/user with admin credentials -> 200 and login field. /// GET /api/v1/user with admin credentials -> 200 and login field.
async fn check_gitea_auth(domain: &str, client: &reqwest::Client) -> CheckResult { async fn check_gitea_auth(domain: &str, client: &reqwest::Client) -> CheckResult {
let username = { let username = {
let u = kube_secret("devtools", "gitea-admin-credentials", "admin-username").await; let u = kube_secret("devtools", "gitea-admin-credentials", "username").await;
if u.is_empty() { if u.is_empty() {
"gitea_admin".to_string() "gitea_admin".to_string()
} else { } else {
@@ -144,13 +144,13 @@ async fn check_gitea_auth(domain: &str, client: &reqwest::Client) -> CheckResult
} }
}; };
let password = let password =
kube_secret("devtools", "gitea-admin-credentials", "admin-password").await; kube_secret("devtools", "gitea-admin-credentials", "password").await;
if password.is_empty() { if password.is_empty() {
return CheckResult::fail( return CheckResult::fail(
"gitea-auth", "gitea-auth",
"devtools", "devtools",
"gitea", "gitea",
"admin-password not found in secret", "password not found in secret",
); );
} }
@@ -895,7 +895,7 @@ mod tests {
"gitea-auth", "gitea-auth",
"devtools", "devtools",
"gitea", "gitea",
"admin-password not found in secret", "password not found in secret",
); );
assert!(!r.passed); assert!(!r.passed);
assert!(r.detail.contains("secret")); assert!(r.detail.contains("secret"));

View File

@@ -2,11 +2,10 @@
//! //!
//! Pure K8s implementation: no Lima VM operations. //! Pure K8s implementation: no Lima VM operations.
use crate::constants::GITEA_ADMIN_USER;
use crate::error::{Result, ResultExt, SunbeamError}; use crate::error::{Result, ResultExt, SunbeamError};
use std::path::PathBuf; use std::path::PathBuf;
const GITEA_ADMIN_USER: &str = "gitea_admin";
const CERT_MANAGER_URL: &str = const CERT_MANAGER_URL: &str =
"https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml"; "https://github.com/cert-manager/cert-manager/releases/download/v1.17.0/cert-manager.yaml";
@@ -161,6 +160,12 @@ async fn ensure_tls_cert(domain: &str) -> Result<()> {
std::fs::write(&key_path, key_pair.serialize_pem()) std::fs::write(&key_path, key_pair.serialize_pem())
.with_ctx(|| format!("Failed to write {}", key_path.display()))?; .with_ctx(|| format!("Failed to write {}", key_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600))?;
}
crate::output::ok(&format!("Cert generated. Domain: {domain}")); crate::output::ok(&format!("Cert generated. Domain: {domain}"));
Ok(()) Ok(())
} }
@@ -237,7 +242,7 @@ async fn wait_for_core() -> Result<()> {
// Print URLs // Print URLs
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
fn print_urls(domain: &str, gitea_admin_pass: &str) { fn print_urls(domain: &str, _gitea_admin_pass: &str) {
let sep = "\u{2500}".repeat(60); let sep = "\u{2500}".repeat(60);
println!("\n{sep}"); println!("\n{sep}");
println!(" Stack is up. Domain: {domain}"); println!(" Stack is up. Domain: {domain}");
@@ -254,7 +259,7 @@ fn print_urls(domain: &str, gitea_admin_pass: &str) {
( (
"Gitea", "Gitea",
format!( format!(
"https://src.{domain}/ ({GITEA_ADMIN_USER} / {gitea_admin_pass})" "https://src.{domain}/ ({GITEA_ADMIN_USER} / <from openbao>)"
), ),
), ),
]; ];
@@ -446,12 +451,11 @@ mod tests {
#[test] #[test]
fn print_urls_gitea_includes_credentials() { fn print_urls_gitea_includes_credentials() {
let domain = "example.local"; let domain = "example.local";
let pass = "s3cret";
let gitea_url = format!( let gitea_url = format!(
"https://src.{domain}/ ({GITEA_ADMIN_USER} / {pass})" "https://src.{domain}/ ({GITEA_ADMIN_USER} / <from openbao>)"
); );
assert!(gitea_url.contains(GITEA_ADMIN_USER)); assert!(gitea_url.contains(GITEA_ADMIN_USER));
assert!(gitea_url.contains(pass)); assert!(gitea_url.contains("<from openbao>"));
assert!(gitea_url.contains(&format!("src.{domain}"))); assert!(gitea_url.contains(&format!("src.{domain}")));
} }
} }

16
src/constants.rs Normal file
View File

@@ -0,0 +1,16 @@
//! Shared constants used across multiple modules.
pub const GITEA_ADMIN_USER: &str = "gitea_admin";
pub const MANAGED_NS: &[&str] = &[
"data",
"devtools",
"ingress",
"lasuite",
"matrix",
"media",
"monitoring",
"ory",
"storage",
"vault-secrets-operator",
];

View File

@@ -7,22 +7,9 @@ use std::path::{Path, PathBuf};
use std::process::Stdio; use std::process::Stdio;
use crate::cli::BuildTarget; use crate::cli::BuildTarget;
use crate::constants::{GITEA_ADMIN_USER, MANAGED_NS};
use crate::output::{ok, step, warn}; use crate::output::{ok, step, warn};
const GITEA_ADMIN_USER: &str = "gitea_admin";
const MANAGED_NS: &[&str] = &[
"data",
"devtools",
"ingress",
"lasuite",
"matrix",
"media",
"ory",
"storage",
"vault-secrets-operator",
];
/// amd64-only images that need mirroring: (source, org, repo, tag). /// amd64-only images that need mirroring: (source, org, repo, tag).
const AMD64_ONLY_IMAGES: &[(&str, &str, &str, &str)] = &[ const AMD64_ONLY_IMAGES: &[(&str, &str, &str, &str)] = &[
( (

View File

@@ -4,6 +4,7 @@ mod error;
mod checks; mod checks;
mod cli; mod cli;
mod cluster; mod cluster;
mod constants;
mod config; mod config;
mod gitea; mod gitea;
mod images; mod images;

View File

@@ -1,17 +1,5 @@
use crate::error::Result; use crate::error::Result;
use crate::constants::MANAGED_NS;
pub const MANAGED_NS: &[&str] = &[
"data",
"devtools",
"ingress",
"lasuite",
"matrix",
"media",
"monitoring",
"ory",
"storage",
"vault-secrets-operator",
];
/// Return only the YAML documents that belong to the given namespace. /// Return only the YAML documents that belong to the given namespace.
pub fn filter_by_namespace(manifests: &str, namespace: &str) -> String { pub fn filter_by_namespace(manifests: &str, namespace: &str) -> String {
@@ -109,9 +97,7 @@ pub async fn cmd_apply(env: &str, domain: &str, email: &str, namespace: &str) ->
// If cert-manager is in the overlay, wait for its webhook then re-apply // If cert-manager is in the overlay, wait for its webhook then re-apply
let cert_manager_present = overlay let cert_manager_present = overlay
.join("../../base/cert-manager") .join("../../base/cert-manager")
.canonicalize() .exists();
.map(|p| p.exists())
.unwrap_or(false);
if cert_manager_present && namespace.is_empty() { if cert_manager_present && namespace.is_empty() {
if wait_for_webhook("cert-manager", "cert-manager-webhook", 120).await { if wait_for_webhook("cert-manager", "cert-manager-webhook", 120).await {
@@ -149,11 +135,18 @@ async fn pre_apply_cleanup(namespaces: Option<&[String]>) {
}; };
crate::output::ok("Cleaning up immutable Jobs and test Pods..."); crate::output::ok("Cleaning up immutable Jobs and test Pods...");
// Prune stale VaultStaticSecrets that share a name with VaultDynamicSecrets
prune_stale_vault_static_secrets(&ns_list).await;
for ns in &ns_list { for ns in &ns_list {
// Delete all jobs // Delete all jobs
let client = match crate::kube::get_client().await { let client = match crate::kube::get_client().await {
Ok(c) => c, Ok(c) => c,
Err(_) => return, Err(e) => {
crate::output::warn(&format!("Failed to get kube client: {e}"));
return;
}
}; };
let jobs: kube::api::Api<k8s_openapi::api::batch::v1::Job> = let jobs: kube::api::Api<k8s_openapi::api::batch::v1::Job> =
kube::api::Api::namespaced(client.clone(), ns); kube::api::Api::namespaced(client.clone(), ns);
@@ -185,6 +178,67 @@ async fn pre_apply_cleanup(namespaces: Option<&[String]>) {
} }
} }
/// Prune VaultStaticSecrets that share a name with VaultDynamicSecrets in the same namespace.
async fn prune_stale_vault_static_secrets(namespaces: &[&str]) {
let client = match crate::kube::get_client().await {
Ok(c) => c,
Err(e) => {
crate::output::warn(&format!("Failed to get kube client for VSS pruning: {e}"));
return;
}
};
let vss_ar = kube::api::ApiResource {
group: "secrets.hashicorp.com".into(),
version: "v1beta1".into(),
api_version: "secrets.hashicorp.com/v1beta1".into(),
kind: "VaultStaticSecret".into(),
plural: "vaultstaticsecrets".into(),
};
let vds_ar = kube::api::ApiResource {
group: "secrets.hashicorp.com".into(),
version: "v1beta1".into(),
api_version: "secrets.hashicorp.com/v1beta1".into(),
kind: "VaultDynamicSecret".into(),
plural: "vaultdynamicsecrets".into(),
};
for ns in namespaces {
let vss_api: kube::api::Api<kube::api::DynamicObject> =
kube::api::Api::namespaced_with(client.clone(), ns, &vss_ar);
let vds_api: kube::api::Api<kube::api::DynamicObject> =
kube::api::Api::namespaced_with(client.clone(), ns, &vds_ar);
let vss_list = match vss_api.list(&kube::api::ListParams::default()).await {
Ok(l) => l,
Err(_) => continue,
};
let vds_list = match vds_api.list(&kube::api::ListParams::default()).await {
Ok(l) => l,
Err(_) => continue,
};
let vds_names: std::collections::HashSet<String> = vds_list
.items
.iter()
.filter_map(|o| o.metadata.name.clone())
.collect();
for vss in &vss_list.items {
if let Some(name) = &vss.metadata.name {
if vds_names.contains(name) {
crate::output::ok(&format!(
"Pruning stale VaultStaticSecret {ns}/{name} (replaced by VaultDynamicSecret)"
));
let dp = kube::api::DeleteParams::default();
let _ = vss_api.delete(name, &dp).await;
}
}
}
}
}
/// Snapshot ConfigMap resourceVersions across managed namespaces. /// Snapshot ConfigMap resourceVersions across managed namespaces.
async fn snapshot_configmaps() -> std::collections::HashMap<String, String> { async fn snapshot_configmaps() -> std::collections::HashMap<String, String> {
let mut result = std::collections::HashMap::new(); let mut result = std::collections::HashMap::new();
@@ -422,8 +476,7 @@ async fn os_api(path: &str, method: &str, body: Option<&str>) -> Option<String>
} }
// Build the full exec command: exec deploy/opensearch -n data -c opensearch -- curl ... // Build the full exec command: exec deploy/opensearch -n data -c opensearch -- curl ...
let mut exec_cmd: Vec<&str> = vec!["curl"]; let exec_cmd = curl_args;
exec_cmd = curl_args;
match crate::kube::kube_exec("data", "opensearch-0", &exec_cmd, Some("opensearch")).await { match crate::kube::kube_exec("data", "opensearch-0", &exec_cmd, Some("opensearch")).await {
Ok((0, out)) if !out.is_empty() => Some(out), Ok((0, out)) if !out.is_empty() => Some(out),

View File

@@ -5,22 +5,10 @@ use k8s_openapi::api::core::v1::Pod;
use kube::api::{Api, DynamicObject, ListParams, LogParams}; use kube::api::{Api, DynamicObject, ListParams, LogParams};
use kube::ResourceExt; use kube::ResourceExt;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use crate::constants::MANAGED_NS;
use crate::kube::{get_client, kube_rollout_restart, parse_target}; use crate::kube::{get_client, kube_rollout_restart, parse_target};
use crate::output::{ok, step, warn}; use crate::output::{ok, step, warn};
/// Namespaces managed by sunbeam.
pub const MANAGED_NS: &[&str] = &[
"data",
"devtools",
"ingress",
"lasuite",
"matrix",
"media",
"ory",
"storage",
"vault-secrets-operator",
];
/// Services that can be rollout-restarted, as (namespace, deployment) pairs. /// Services that can be rollout-restarted, as (namespace, deployment) pairs.
pub const SERVICES_TO_RESTART: &[(&str, &str)] = &[ pub const SERVICES_TO_RESTART: &[(&str, &str)] = &[
("ory", "hydra"), ("ory", "hydra"),
@@ -462,8 +450,9 @@ mod tests {
assert!(MANAGED_NS.contains(&"matrix")); assert!(MANAGED_NS.contains(&"matrix"));
assert!(MANAGED_NS.contains(&"media")); assert!(MANAGED_NS.contains(&"media"));
assert!(MANAGED_NS.contains(&"storage")); assert!(MANAGED_NS.contains(&"storage"));
assert!(MANAGED_NS.contains(&"monitoring"));
assert!(MANAGED_NS.contains(&"vault-secrets-operator")); assert!(MANAGED_NS.contains(&"vault-secrets-operator"));
assert_eq!(MANAGED_NS.len(), 9); assert_eq!(MANAGED_NS.len(), 10);
} }
#[test] #[test]