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:
@@ -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"));
|
||||
|
||||
@@ -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
16
src/constants.rs
Normal 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",
|
||||
];
|
||||
@@ -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)] = &[
|
||||
(
|
||||
|
||||
@@ -4,6 +4,7 @@ mod error;
|
||||
mod checks;
|
||||
mod cli;
|
||||
mod cluster;
|
||||
mod constants;
|
||||
mod config;
|
||||
mod gitea;
|
||||
mod images;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user