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.
|
/// 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"));
|
||||||
|
|||||||
@@ -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
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 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)] = &[
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user