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.
async fn check_gitea_auth(domain: &str, client: &reqwest::Client) -> CheckResult {
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() {
"gitea_admin".to_string()
} else {
@@ -144,13 +144,13 @@ async fn check_gitea_auth(domain: &str, client: &reqwest::Client) -> CheckResult
}
};
let password =
kube_secret("devtools", "gitea-admin-credentials", "admin-password").await;
kube_secret("devtools", "gitea-admin-credentials", "password").await;
if password.is_empty() {
return CheckResult::fail(
"gitea-auth",
"devtools",
"gitea",
"admin-password not found in secret",
"password not found in secret",
);
}
@@ -895,7 +895,7 @@ mod tests {
"gitea-auth",
"devtools",
"gitea",
"admin-password not found in secret",
"password not found in secret",
);
assert!(!r.passed);
assert!(r.detail.contains("secret"));

View File

@@ -2,11 +2,10 @@
//!
//! Pure K8s implementation: no Lima VM operations.
use crate::constants::GITEA_ADMIN_USER;
use crate::error::{Result, ResultExt, SunbeamError};
use std::path::PathBuf;
const GITEA_ADMIN_USER: &str = "gitea_admin";
const CERT_MANAGER_URL: &str =
"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())
.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}"));
Ok(())
}
@@ -237,7 +242,7 @@ async fn wait_for_core() -> Result<()> {
// 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);
println!("\n{sep}");
println!(" Stack is up. Domain: {domain}");
@@ -254,7 +259,7 @@ fn print_urls(domain: &str, gitea_admin_pass: &str) {
(
"Gitea",
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]
fn print_urls_gitea_includes_credentials() {
let domain = "example.local";
let pass = "s3cret";
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(pass));
assert!(gitea_url.contains("<from openbao>"));
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 crate::cli::BuildTarget;
use crate::constants::{GITEA_ADMIN_USER, MANAGED_NS};
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).
const AMD64_ONLY_IMAGES: &[(&str, &str, &str, &str)] = &[
(

View File

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

View File

@@ -1,17 +1,5 @@
use crate::error::Result;
pub const MANAGED_NS: &[&str] = &[
"data",
"devtools",
"ingress",
"lasuite",
"matrix",
"media",
"monitoring",
"ory",
"storage",
"vault-secrets-operator",
];
use crate::constants::MANAGED_NS;
/// Return only the YAML documents that belong to the given namespace.
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
let cert_manager_present = overlay
.join("../../base/cert-manager")
.canonicalize()
.map(|p| p.exists())
.unwrap_or(false);
.exists();
if cert_manager_present && namespace.is_empty() {
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...");
// Prune stale VaultStaticSecrets that share a name with VaultDynamicSecrets
prune_stale_vault_static_secrets(&ns_list).await;
for ns in &ns_list {
// Delete all jobs
let client = match crate::kube::get_client().await {
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> =
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.
async fn snapshot_configmaps() -> std::collections::HashMap<String, String> {
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 ...
let mut exec_cmd: Vec<&str> = vec!["curl"];
exec_cmd = curl_args;
let exec_cmd = curl_args;
match crate::kube::kube_exec("data", "opensearch-0", &exec_cmd, Some("opensearch")).await {
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::ResourceExt;
use std::collections::BTreeMap;
use crate::constants::MANAGED_NS;
use crate::kube::{get_client, kube_rollout_restart, parse_target};
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.
pub const SERVICES_TO_RESTART: &[(&str, &str)] = &[
("ory", "hydra"),
@@ -462,8 +450,9 @@ mod tests {
assert!(MANAGED_NS.contains(&"matrix"));
assert!(MANAGED_NS.contains(&"media"));
assert!(MANAGED_NS.contains(&"storage"));
assert!(MANAGED_NS.contains(&"monitoring"));
assert!(MANAGED_NS.contains(&"vault-secrets-operator"));
assert_eq!(MANAGED_NS.len(), 9);
assert_eq!(MANAGED_NS.len(), 10);
}
#[test]