diff --git a/src/checks.rs b/src/checks.rs index f40e899..5deaff0 100644 --- a/src/checks.rs +++ b/src/checks.rs @@ -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")); diff --git a/src/cluster.rs b/src/cluster.rs index fdedd88..e5de3cf 100644 --- a/src/cluster.rs +++ b/src/cluster.rs @@ -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} / )" ), ), ]; @@ -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} / )" ); assert!(gitea_url.contains(GITEA_ADMIN_USER)); - assert!(gitea_url.contains(pass)); + assert!(gitea_url.contains("")); assert!(gitea_url.contains(&format!("src.{domain}"))); } } diff --git a/src/constants.rs b/src/constants.rs new file mode 100644 index 0000000..5ab992e --- /dev/null +++ b/src/constants.rs @@ -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", +]; diff --git a/src/images.rs b/src/images.rs index 6064a87..4cb62fd 100644 --- a/src/images.rs +++ b/src/images.rs @@ -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)] = &[ ( diff --git a/src/main.rs b/src/main.rs index 290761f..b5b62e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod error; mod checks; mod cli; mod cluster; +mod constants; mod config; mod gitea; mod images; diff --git a/src/manifests.rs b/src/manifests.rs index 5bcfe9f..f0761b9 100644 --- a/src/manifests.rs +++ b/src/manifests.rs @@ -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 = 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::Api::namespaced_with(client.clone(), ns, &vss_ar); + let vds_api: kube::api::Api = + 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 = 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 { let mut result = std::collections::HashMap::new(); @@ -422,8 +476,7 @@ async fn os_api(path: &str, method: &str, body: Option<&str>) -> Option } // 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), diff --git a/src/services.rs b/src/services.rs index e3a8807..1f69109 100644 --- a/src/services.rs +++ b/src/services.rs @@ -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]