diff --git a/Cargo.lock b/Cargo.lock index fb5288e6..debcaa53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1313,7 +1313,7 @@ version = "0.1.0" dependencies = [ "async-trait", "env_logger", - "harmony_secret", + "harmony_config", "harmony_types", "log", "regex", @@ -3181,9 +3181,8 @@ dependencies = [ "env_logger", "harmony", "harmony_cli", + "harmony_config", "harmony_macros", - "harmony_secret", - "harmony_secret_derive", "harmony_types", "log", "schemars 0.8.22", @@ -3240,8 +3239,8 @@ dependencies = [ "env_logger", "harmony", "harmony_cli", + "harmony_config", "harmony_macros", - "harmony_secret", "harmony_types", "log", "schemars 0.8.22", @@ -3320,9 +3319,8 @@ dependencies = [ "env_logger", "harmony", "harmony_cli", + "harmony_config", "harmony_macros", - "harmony_secret", - "harmony_secret_derive", "harmony_types", "log", "schemars 0.8.22", @@ -3465,7 +3463,7 @@ dependencies = [ "harmony", "harmony-fleet-deploy", "harmony_cli", - "harmony_secret", + "harmony_config", "harmony_types", "log", "reqwest 0.12.28", @@ -6445,9 +6443,8 @@ dependencies = [ "cidr", "harmony", "harmony_cli", + "harmony_config", "harmony_macros", - "harmony_secret", - "harmony_secret_derive", "harmony_types", "schemars 0.8.22", "serde", @@ -8896,8 +8893,8 @@ dependencies = [ "cidr", "harmony", "harmony_cli", + "harmony_config", "harmony_macros", - "harmony_secret", "tokio", ] diff --git a/Dockerfile b/Dockerfile index 0d98f22b..a5255139 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM docker.io/rust:1.94 AS build +FROM docker.io/rust:1.97 AS build WORKDIR /app diff --git a/brocade/Cargo.toml b/brocade/Cargo.toml index 5fe0bd1e..f27083dd 100644 --- a/brocade/Cargo.toml +++ b/brocade/Cargo.toml @@ -14,6 +14,6 @@ tokio.workspace = true log.workspace = true env_logger.workspace = true regex = "1.11.3" -harmony_secret = { path = "../harmony_secret" } +harmony_config = { path = "../harmony_config" } serde.workspace = true schemars = "0.8" diff --git a/brocade/examples/main.rs b/brocade/examples/main.rs index 6999c018..c83f7929 100644 --- a/brocade/examples/main.rs +++ b/brocade/examples/main.rs @@ -1,12 +1,13 @@ use std::net::{IpAddr, Ipv4Addr}; use brocade::{BrocadeOptions, ssh}; -use harmony_secret::{Secret, SecretManager}; +use harmony_config::Config; use harmony_types::switch::PortLocation; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Secret, Clone, Debug, JsonSchema, Serialize, Deserialize)] +#[derive(Config, Clone, Debug, JsonSchema, Serialize, Deserialize)] +#[config(secret)] struct BrocadeSwitchAuth { username: String, password: String, @@ -21,7 +22,7 @@ async fn main() { // let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st let switch_addresses = vec![ip]; - let config = SecretManager::get_or_prompt::() + let config = harmony_config::get_or_prompt::() .await .unwrap(); diff --git a/examples/application_monitoring_with_tenant/src/main.rs b/examples/application_monitoring_with_tenant/src/main.rs index 38fe6c5e..353c1c8e 100644 --- a/examples/application_monitoring_with_tenant/src/main.rs +++ b/examples/application_monitoring_with_tenant/src/main.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] use std::{path::PathBuf, str::FromStr, sync::Arc}; use harmony::{ diff --git a/examples/fleet_rpi_setup/Cargo.toml b/examples/fleet_rpi_setup/Cargo.toml index c72ee3ec..833425b2 100644 --- a/examples/fleet_rpi_setup/Cargo.toml +++ b/examples/fleet_rpi_setup/Cargo.toml @@ -12,7 +12,7 @@ path = "src/main.rs" harmony = { path = "../../harmony" } harmony-fleet-deploy = { path = "../../fleet/harmony-fleet-deploy" } harmony_cli = { path = "../../harmony_cli" } -harmony_secret = { path = "../../harmony_secret" } +harmony_config = { path = "../../harmony_config" } harmony_types = { path = "../../harmony_types" } tokio.workspace = true log.workspace = true diff --git a/examples/fleet_rpi_setup/src/main.rs b/examples/fleet_rpi_setup/src/main.rs index 81068808..e62dfbfb 100644 --- a/examples/fleet_rpi_setup/src/main.rs +++ b/examples/fleet_rpi_setup/src/main.rs @@ -23,7 +23,7 @@ //! - SSH server enabled //! - An admin user with sudo. Passwordless sudo is detected and //! used silently; otherwise the example prompts for a sudo -//! password via `SecretManager` and caches it for next runs. +//! password via `harmony_config` and caches it for next runs. //! - Your driver-machine SSH public key in that user's //! `~/.ssh/authorized_keys` //! @@ -39,7 +39,6 @@ use harmony::config::secret::SudoPassword; use harmony::inventory::Inventory; use harmony::modules::linux::{LinuxHostTopology, SshCredentials, ensure_ansible_venv, ssh_exec}; use harmony_fleet_deploy::{FleetDeviceAuth, FleetDeviceSetupConfig, FleetDeviceSetupScore}; -use harmony_secret::SecretManager; use harmony_types::id::Id; use log::info; use std::path::PathBuf; @@ -144,8 +143,8 @@ async fn main() -> Result<()> { }; // If the Pi doesn't have passwordless sudo, fetch the password - // through SecretManager (same flow other scores use for SSH keys - // etc. — see harmony_secret/src/lib.rs:145). First run prompts; + // through harmony_config (same flow other scores use for SSH keys + // etc.). First run prompts; // subsequent runs reuse the cached value. Probe with `sudo -n` // first so we don't prompt the operator for a password they // don't need. @@ -154,7 +153,7 @@ async fn main() -> Result<()> { .map_err(|e| anyhow::anyhow!("sudo probe: {e}"))?; if probe.rc != 0 { info!("device requires a sudo password — fetching from secret store"); - let secret = SecretManager::get_or_prompt::() + let secret = harmony_config::get_or_prompt::() .await .map_err(|e| anyhow::anyhow!("get sudo password: {e}"))?; creds.sudo_password = Some(secret.password); diff --git a/examples/okd_add_node/Cargo.toml b/examples/okd_add_node/Cargo.toml index 8b063c00..43255fd3 100644 --- a/examples/okd_add_node/Cargo.toml +++ b/examples/okd_add_node/Cargo.toml @@ -11,8 +11,7 @@ harmony = { path = "../../harmony" } harmony_cli = { path = "../../harmony_cli" } harmony_macros = { path = "../../harmony_macros" } harmony_types = { path = "../../harmony_types" } -harmony_secret = { path = "../../harmony_secret" } -harmony_secret_derive = { path = "../../harmony_secret_derive" } +harmony_config = { path = "../../harmony_config" } tokio.workspace = true cidr.workspace = true serde = { workspace = true } diff --git a/examples/okd_add_node/src/topology.rs b/examples/okd_add_node/src/topology.rs index 28ed6647..554dbe35 100644 --- a/examples/okd_add_node/src/topology.rs +++ b/examples/okd_add_node/src/topology.rs @@ -9,8 +9,8 @@ use harmony::{ inventory::Inventory, topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, }; +use harmony_config::Config; use harmony_macros::{ip, ipv4}; -use harmony_secret::{Secret, SecretManager}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ @@ -18,7 +18,8 @@ use std::{ sync::{Arc, OnceLock}, }; -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[config(secret)] struct OkdAddNodeFirewallConfig { username: String, password: String, @@ -41,7 +42,7 @@ pub async fn get_topology() -> HAClusterTopology { .expect("Failed to connect to switch"), ); - let config = SecretManager::get_or_prompt::() + let config = harmony_config::get_or_prompt::() .await .unwrap(); let api_creds = harmony::config::secret::OPNSenseApiCredentials { diff --git a/examples/okd_installation/Cargo.toml b/examples/okd_installation/Cargo.toml index 83a3ef5a..d5899dd8 100644 --- a/examples/okd_installation/Cargo.toml +++ b/examples/okd_installation/Cargo.toml @@ -10,8 +10,7 @@ publish = false harmony = { path = "../../harmony" } harmony_cli = { path = "../../harmony_cli" } harmony_types = { path = "../../harmony_types" } -harmony_secret = { path = "../../harmony_secret" } -harmony_secret_derive = { path = "../../harmony_secret_derive" } +harmony_config = { path = "../../harmony_config" } cidr = { workspace = true } tokio = { workspace = true } harmony_macros = { path = "../../harmony_macros" } diff --git a/examples/okd_installation/src/main.rs b/examples/okd_installation/src/main.rs index bf00e511..414a5c87 100644 --- a/examples/okd_installation/src/main.rs +++ b/examples/okd_installation/src/main.rs @@ -14,14 +14,13 @@ use harmony::{ score::Score, topology::HAClusterTopology, }; -use harmony_secret::SecretManager; #[tokio::main] async fn main() { let inventory = get_inventory(); let topology = get_topology().await; - let ssh_key = SecretManager::get_or_prompt::().await.unwrap(); + let ssh_key = harmony_config::get_or_prompt::().await.unwrap(); let mut scores: Vec>> = vec![Box::new(OKDIpxeScore { kickstart_filename: "inventory.kickstart".to_string(), diff --git a/examples/okd_installation/src/topology.rs b/examples/okd_installation/src/topology.rs index d96c0b62..1fcc7a4d 100644 --- a/examples/okd_installation/src/topology.rs +++ b/examples/okd_installation/src/topology.rs @@ -12,7 +12,6 @@ use harmony::{ topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, }; use harmony_macros::{ip, ipv4}; -use harmony_secret::SecretManager; use std::{ net::IpAddr, sync::{Arc, OnceLock}, @@ -24,7 +23,7 @@ pub async fn get_topology() -> HAClusterTopology { name: String::from("opnsense-1"), }; - let switch_auth = SecretManager::get_or_prompt::() + let switch_auth = harmony_config::get_or_prompt::() .await .expect("Failed to get credentials"); @@ -43,10 +42,10 @@ pub async fn get_topology() -> HAClusterTopology { let switch_client = Arc::new(switch_client); - let ssh_creds = SecretManager::get_or_prompt::() + let ssh_creds = harmony_config::get_or_prompt::() .await .unwrap(); - let api_creds = SecretManager::get_or_prompt::() + let api_creds = harmony_config::get_or_prompt::() .await .unwrap(); diff --git a/examples/okd_pxe/Cargo.toml b/examples/okd_pxe/Cargo.toml index 446133c1..acb86590 100644 --- a/examples/okd_pxe/Cargo.toml +++ b/examples/okd_pxe/Cargo.toml @@ -10,8 +10,7 @@ publish = false harmony = { path = "../../harmony" } harmony_cli = { path = "../../harmony_cli" } harmony_types = { path = "../../harmony_types" } -harmony_secret = { path = "../../harmony_secret" } -harmony_secret_derive = { path = "../../harmony_secret_derive" } +harmony_config = { path = "../../harmony_config" } cidr = { workspace = true } tokio = { workspace = true } harmony_macros = { path = "../../harmony_macros" } diff --git a/examples/okd_pxe/src/main.rs b/examples/okd_pxe/src/main.rs index bd638dd5..a9af2cde 100644 --- a/examples/okd_pxe/src/main.rs +++ b/examples/okd_pxe/src/main.rs @@ -6,7 +6,6 @@ use harmony::{ data::{FileContent, FilePath}, modules::okd::ipxe::OKDIpxeScore, }; -use harmony_secret::SecretManager; #[tokio::main] async fn main() { @@ -15,7 +14,7 @@ async fn main() { let kickstart_filename = "inventory.kickstart".to_string(); let harmony_inventory_agent = "harmony_inventory_agent".to_string(); - let ssh_key = SecretManager::get_or_prompt::().await.unwrap(); + let ssh_key = harmony_config::get_or_prompt::().await.unwrap(); let ipxe_score = OKDIpxeScore { kickstart_filename, diff --git a/examples/okd_pxe/src/topology.rs b/examples/okd_pxe/src/topology.rs index 3f618df0..f90d4394 100644 --- a/examples/okd_pxe/src/topology.rs +++ b/examples/okd_pxe/src/topology.rs @@ -12,7 +12,6 @@ use harmony::{ topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, }; use harmony_macros::{ip, ipv4}; -use harmony_secret::SecretManager; use std::{ net::IpAddr, sync::{Arc, OnceLock}, @@ -24,7 +23,7 @@ pub async fn get_topology() -> HAClusterTopology { name: String::from("opnsense-1"), }; - let switch_auth = SecretManager::get_or_prompt::() + let switch_auth = harmony_config::get_or_prompt::() .await .expect("Failed to get credentials"); @@ -43,11 +42,11 @@ pub async fn get_topology() -> HAClusterTopology { let switch_client = Arc::new(switch_client); - let ssh_creds = SecretManager::get_or_prompt::() + let ssh_creds = harmony_config::get_or_prompt::() .await .unwrap(); let api_creds = - SecretManager::get_or_prompt::() + harmony_config::get_or_prompt::() .await .unwrap(); diff --git a/examples/opnsense/Cargo.toml b/examples/opnsense/Cargo.toml index 4b6ae189..63a303e2 100644 --- a/examples/opnsense/Cargo.toml +++ b/examples/opnsense/Cargo.toml @@ -16,7 +16,7 @@ harmony_macros = { path = "../../harmony_macros" } log = { workspace = true } env_logger = { workspace = true } url = { workspace = true } -harmony_secret = { path = "../../harmony_secret" } +harmony_config = { path = "../../harmony_config" } brocade = { path = "../../brocade" } serde = { workspace = true } schemars = "0.8" diff --git a/examples/opnsense/src/main.rs b/examples/opnsense/src/main.rs index 2b02daad..519abbe3 100644 --- a/examples/opnsense/src/main.rs +++ b/examples/opnsense/src/main.rs @@ -6,9 +6,6 @@ use harmony::{ topology::LogicalHost, }; use harmony_macros::{ip, ipv4}; -use harmony_secret::{Secret, SecretManager}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; #[tokio::main] async fn main() { @@ -17,10 +14,10 @@ async fn main() { name: String::from("opnsense-1"), }; - let ssh_creds = SecretManager::get_or_prompt::() + let ssh_creds = harmony_config::get_or_prompt::() .await .expect("Failed to get SSH credentials"); - let api_creds = SecretManager::get_or_prompt::() + let api_creds = harmony_config::get_or_prompt::() .await .expect("Failed to get API credentials"); @@ -67,9 +64,3 @@ async fn main() { .await .unwrap(); } - -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug)] -pub struct BrocadeSwitchAuth { - pub username: String, - pub password: String, -} diff --git a/examples/rhob_application_monitoring/src/main.rs b/examples/rhob_application_monitoring/src/main.rs index 5d810f7e..ed28c6d6 100644 --- a/examples/rhob_application_monitoring/src/main.rs +++ b/examples/rhob_application_monitoring/src/main.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] use std::{path::PathBuf, sync::Arc}; use harmony::{ diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index 2119ea94..6e81c953 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] use std::{path::PathBuf, sync::Arc}; use harmony::{ diff --git a/examples/sttest/Cargo.toml b/examples/sttest/Cargo.toml index ade085f9..fe16fdbf 100644 --- a/examples/sttest/Cargo.toml +++ b/examples/sttest/Cargo.toml @@ -10,6 +10,6 @@ publish = false harmony = { path = "../../harmony" } harmony_cli = { path = "../../harmony_cli" } harmony_macros = { path = "../../harmony_macros" } -harmony_secret = { path = "../../harmony_secret" } +harmony_config = { path = "../../harmony_config" } cidr = { workspace = true } tokio = { workspace = true } diff --git a/examples/sttest/src/main.rs b/examples/sttest/src/main.rs index c5f7d02a..8b1ecf9d 100644 --- a/examples/sttest/src/main.rs +++ b/examples/sttest/src/main.rs @@ -16,7 +16,6 @@ use harmony::{ topology::HAClusterTopology, }; use harmony_macros::cidrv4; -use harmony_secret::SecretManager; #[tokio::main] async fn main() { @@ -25,7 +24,7 @@ async fn main() { let inventory = get_inventory(); let topology = get_topology().await; - let ssh_key = SecretManager::get_or_prompt::().await.unwrap(); + let ssh_key = harmony_config::get_or_prompt::().await.unwrap(); // Discovery runs as a CIDR scan across the sttest LAN on the // harmony_inventory_agent's default port. Shared between the install diff --git a/examples/sttest/src/topology.rs b/examples/sttest/src/topology.rs index ff3eebca..705a0559 100644 --- a/examples/sttest/src/topology.rs +++ b/examples/sttest/src/topology.rs @@ -7,7 +7,6 @@ use harmony::{ topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, }; use harmony_macros::{ip, ipv4}; -use harmony_secret::SecretManager; use std::{ net::IpAddr, sync::{Arc, OnceLock}, @@ -22,10 +21,10 @@ pub async fn get_opnsense() -> Arc { name: String::from("fw0"), }; - let ssh_creds = SecretManager::get_or_prompt::() + let ssh_creds = harmony_config::get_or_prompt::() .await .unwrap(); - let api_credentials = SecretManager::get_or_prompt::() + let api_credentials = harmony_config::get_or_prompt::() .await .unwrap(); diff --git a/examples/try_rust_webapp/files_to_add/main.rs b/examples/try_rust_webapp/files_to_add/main.rs index 7f1c3613..45387ce7 100644 --- a/examples/try_rust_webapp/files_to_add/main.rs +++ b/examples/try_rust_webapp/files_to_add/main.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] use harmony::{ inventory::Inventory, modules::{ diff --git a/examples/try_rust_webapp/src/main.rs b/examples/try_rust_webapp/src/main.rs index a7ab54b4..d1973419 100644 --- a/examples/try_rust_webapp/src/main.rs +++ b/examples/try_rust_webapp/src/main.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] use harmony::{ inventory::Inventory, modules::{ diff --git a/harmony-k8s/src/discovery.rs b/harmony-k8s/src/discovery.rs index 9c865dc5..2444b701 100644 --- a/harmony-k8s/src/discovery.rs +++ b/harmony-k8s/src/discovery.rs @@ -33,7 +33,7 @@ impl K8sClient { .take(6); let attempt = Mutex::new(0u32); - let d = Retry::spawn(retry_strategy, || async { + let d = Retry::start(retry_strategy, || async { let mut n = attempt.lock().await; *n += 1; debug!("Running Kubernetes API discovery (attempt {})", *n); diff --git a/harmony/src/domain/config/secret.rs b/harmony/src/domain/config/secret.rs index 44037d2e..6b816cd4 100644 --- a/harmony/src/domain/config/secret.rs +++ b/harmony/src/domain/config/secret.rs @@ -1,27 +1,31 @@ -use harmony_secret_derive::Secret; +use harmony_config::Config; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[config(secret)] pub struct OPNSenseFirewallCredentials { pub username: String, pub password: String, } -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[config(secret)] pub struct OPNSenseApiCredentials { pub key: String, pub secret: String, } // TODO we need a better way to handle multiple "instances" of the same secret structure. -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[config(secret)] pub struct SshKeyPair { pub private: String, pub public: String, } -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[config(secret)] pub struct RedhatSecret { pub pull_secret: String, } @@ -33,7 +37,8 @@ pub struct RedhatSecret { /// Not used for SSH login itself — `LinuxHostTopology` still requires /// a `private_key_path` for that. SSH password auth is a possible /// future extension; see the TODO on `SshCredentials`. -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] +#[config(secret)] pub struct SudoPassword { pub password: String, } diff --git a/harmony/src/domain/topology/firewall_pair.rs b/harmony/src/domain/topology/firewall_pair.rs index 76bbb53f..267253bb 100644 --- a/harmony/src/domain/topology/firewall_pair.rs +++ b/harmony/src/domain/topology/firewall_pair.rs @@ -35,8 +35,6 @@ use crate::topology::{ Topology, }; -use harmony_secret::SecretManager; - // ── FirewallPairTopology ─────────────────────────────────────────── /// An OPNsense HA firewall pair managed via CARP. @@ -58,16 +56,16 @@ impl FirewallPairTopology { /// - `OPNSENSE_BACKUP_IP` — IP address of the backup firewall /// - `OPNSENSE_API_PORT` — API/web GUI port (default: 443) /// - /// Credentials are loaded via `SecretManager::get_or_prompt`. + /// Credentials are loaded via `harmony_config::get_or_prompt`. pub async fn opnsense_from_config() -> Self { // TODO: both firewalls share the same credentials. Once named config // instances are available (ROADMAP/11), use per-device credentials: // ConfigClient::get_named::("fw-primary") - let ssh_creds = SecretManager::get_or_prompt::() + let ssh_creds = harmony_config::get_or_prompt::() .await .expect("Failed to get SSH credentials"); - let api_creds = SecretManager::get_or_prompt::() + let api_creds = harmony_config::get_or_prompt::() .await .expect("Failed to get API credentials"); diff --git a/harmony/src/infra/networkmanager_cfg.rs b/harmony/src/infra/networkmanager_cfg.rs index 1ebb361e..918c243c 100644 --- a/harmony/src/infra/networkmanager_cfg.rs +++ b/harmony/src/infra/networkmanager_cfg.rs @@ -9,6 +9,7 @@ use harmony_types::firewall::LaggProtocol; /// NetworkManager bond `mode=` value for a `LaggProtocol`. +#[cfg(test)] pub(crate) fn nm_bond_mode(proto: &LaggProtocol) -> &'static str { match proto { LaggProtocol::Lacp => "802.3ad", diff --git a/harmony/src/modules/application/features/monitoring.rs b/harmony/src/modules/application/features/monitoring.rs index 75c6ae5c..3b61d7e2 100644 --- a/harmony/src/modules/application/features/monitoring.rs +++ b/harmony/src/modules/application/features/monitoring.rs @@ -27,8 +27,7 @@ use crate::{ }; use async_trait::async_trait; use base64::{Engine as _, engine::general_purpose}; -use harmony_secret::SecretManager; -use harmony_secret_derive::Secret; +use harmony_config::Config; use harmony_types::net::Url; use kube::api::ObjectMeta; use log::{debug, info}; @@ -100,7 +99,7 @@ impl< .await .map_err(|e| e.to_string())?; - let config = SecretManager::get_or_prompt::().await.unwrap(); + let config = harmony_config::get_or_prompt::().await.unwrap(); let ntfy_default_auth_header = format!( "Basic {}", @@ -143,7 +142,8 @@ impl< } } -#[derive(Secret, Serialize, Deserialize, JsonSchema, Clone, Debug)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Clone, Debug)] +#[config(secret)] struct NtfyAuth { username: String, password: String, diff --git a/harmony/src/modules/brocade/brocade_snmp.rs b/harmony/src/modules/brocade/brocade_snmp.rs index 40b969b9..bb00b31b 100644 --- a/harmony/src/modules/brocade/brocade_snmp.rs +++ b/harmony/src/modules/brocade/brocade_snmp.rs @@ -2,7 +2,7 @@ use std::net::IpAddr; use async_trait::async_trait; use brocade::BrocadeOptions; -use harmony_secret::{Secret, SecretManager}; +use harmony_config::Config; use harmony_types::id::Id; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -38,7 +38,8 @@ pub struct BrocadeEnableSnmpInterpret { score: BrocadeEnableSnmpScore, } -#[derive(Secret, Clone, Debug, JsonSchema, Serialize, Deserialize)] +#[derive(Config, Clone, Debug, JsonSchema, Serialize, Deserialize)] +#[config(secret)] pub struct BrocadeSwitchAuth { pub username: String, pub password: String, @@ -50,7 +51,8 @@ impl BrocadeSwitchAuth { } } -#[derive(Secret, Clone, Debug, JsonSchema, Serialize, Deserialize)] +#[derive(Config, Clone, Debug, JsonSchema, Serialize, Deserialize)] +#[config(secret)] pub struct BrocadeSnmpAuth { pub username: String, pub auth_password: String, @@ -66,11 +68,11 @@ impl Interpret for BrocadeEnableSnmpInterpret { ) -> Result { let switch_addresses = &self.score.switch_ips; - let snmp_auth = SecretManager::get_or_prompt::() + let snmp_auth = harmony_config::get_or_prompt::() .await .unwrap(); - let config = SecretManager::get_or_prompt::() + let config = harmony_config::get_or_prompt::() .await .unwrap(); diff --git a/harmony/src/modules/nats/pki.rs b/harmony/src/modules/nats/pki.rs index 0ac50cc4..26e8e8ad 100644 --- a/harmony/src/modules/nats/pki.rs +++ b/harmony/src/modules/nats/pki.rs @@ -78,7 +78,7 @@ where .max_delay(Duration::from_millis(1000)) .take(10); - Retry::spawn(strategy, || async { + Retry::start(strategy, || async { log::debug!("Attempting CA cert fetch"); let res = self diff --git a/harmony/src/modules/nats/score_nats_k8s.rs b/harmony/src/modules/nats/score_nats_k8s.rs index 272ffc50..3731eee7 100644 --- a/harmony/src/modules/nats/score_nats_k8s.rs +++ b/harmony/src/modules/nats/score_nats_k8s.rs @@ -1,8 +1,8 @@ use std::collections::BTreeMap; use async_trait::async_trait; +use harmony_config::Config; use harmony_k8s::KubernetesDistribution; -use harmony_secret::{Secret, SecretManager}; use harmony_types::id::Id; use k8s_openapi::{ByteString, api::core::v1::Secret as K8sSecret}; use kube::api::ObjectMeta; @@ -344,7 +344,7 @@ impl NatsK8sInterpret { peers: Option>, namespace: String, ) -> Result { - let admin = SecretManager::get_or_prompt::().await.unwrap(); + let admin = harmony_config::get_or_prompt::().await.unwrap(); let admin_user = admin.user.clone(); let admin_password = admin.password.clone(); @@ -582,7 +582,8 @@ config: } } -#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] +#[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq, Clone)] +#[config(secret)] struct NatsAdmin { user: String, password: String, diff --git a/harmony/src/modules/okd/add_node.rs b/harmony/src/modules/okd/add_node.rs index a8c07345..6d0f7fd3 100644 --- a/harmony/src/modules/okd/add_node.rs +++ b/harmony/src/modules/okd/add_node.rs @@ -27,7 +27,7 @@ use harmony_k8s::{ K8sClient, csr::{KUBE_APISERVER_CLIENT_KUBELET_SIGNER, KUBELET_SERVING_SIGNER}, }; -use harmony_secret::SecretManager; + use harmony_types::{id::Id, ssh::SshCredentials}; use k8s_openapi::api::core::v1::{Node, Secret}; use kube::api::ListParams; @@ -349,7 +349,9 @@ impl Interpret for OKDAddNodeInterpret { .map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?; if rebooted { - let ssh_key = SecretManager::get_or_prompt::().await?; + let ssh_key = harmony_config::get_or_prompt::() + .await + .map_err(|e| InterpretError::new(e.to_string()))?; let creds = SshCredentials::SshKey { username: "root".to_string(), private_pem: ssh_key.private, diff --git a/harmony/src/modules/okd/bootstrap_02_bootstrap.rs b/harmony/src/modules/okd/bootstrap_02_bootstrap.rs index c8790238..c63911d7 100644 --- a/harmony/src/modules/okd/bootstrap_02_bootstrap.rs +++ b/harmony/src/modules/okd/bootstrap_02_bootstrap.rs @@ -18,7 +18,7 @@ use crate::{ }; use async_trait::async_trait; use derive_new::new; -use harmony_secret::SecretManager; + use harmony_types::id::Id; use harmony_types::net::Url; use log::{debug, info}; @@ -115,8 +115,12 @@ impl OKDSetup02BootstrapInterpret { ); } - let redhat_secret = SecretManager::get_or_prompt::().await?; - let ssh_key = SecretManager::get_or_prompt::().await?; + let redhat_secret = harmony_config::get_or_prompt::() + .await + .map_err(|e| InterpretError::new(e.to_string()))?; + let ssh_key = harmony_config::get_or_prompt::() + .await + .map_err(|e| InterpretError::new(e.to_string()))?; let install_config_yaml = InstallConfigYaml { cluster_name: &topology.get_cluster_name(), diff --git a/harmony/src/modules/okd/os_artifacts.rs b/harmony/src/modules/okd/os_artifacts.rs index 979a53ea..11bf6fee 100644 --- a/harmony/src/modules/okd/os_artifacts.rs +++ b/harmony/src/modules/okd/os_artifacts.rs @@ -19,7 +19,6 @@ //! regardless of whether the stable name is a regular file or a symlink. use async_trait::async_trait; -use harmony_secret::SecretManager; use harmony_types::{id::Id, ssh::SshCredentials}; use log::{debug, info}; use serde::Serialize; @@ -71,7 +70,9 @@ impl Interpret for OKDOsArtifactsInterpret { topology: &HAClusterTopology, ) -> Result { let fw_ip = topology.firewall.get_ip(); - let fw_creds = SecretManager::get_or_prompt::().await?; + let fw_creds = harmony_config::get_or_prompt::() + .await + .map_err(|e| InterpretError::new(e.to_string()))?; let ssh_creds = SshCredentials::Password { username: fw_creds.username, password: fw_creds.password, diff --git a/harmony/src/modules/zitadel/mod.rs b/harmony/src/modules/zitadel/mod.rs index 62c311a1..fe120571 100644 --- a/harmony/src/modules/zitadel/mod.rs +++ b/harmony/src/modules/zitadel/mod.rs @@ -34,7 +34,7 @@ use std::str::FromStr; use async_trait::async_trait; use harmony_config::{Config, ConfigError, StateClient}; use harmony_types::id::Id; -use log::{debug, error, info, trace, warn}; +use log::{debug, error, info, warn}; use non_blank_string_rs::NonBlankString; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -1078,8 +1078,6 @@ login: } }; - trace!("[Zitadel] Helm values YAML:\n{values_yaml}"); - // --- Step 6: Deploy Helm chart ------------------------------------ let chart_name = diff --git a/harmony_agent/deploy/src/main.rs b/harmony_agent/deploy/src/main.rs index 8baab66b..db9c81cb 100644 --- a/harmony_agent/deploy/src/main.rs +++ b/harmony_agent/deploy/src/main.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] use harmony::{ inventory::Inventory, modules::{ diff --git a/harmony_app/src/application/k8s_anywhere.rs b/harmony_app/src/application/k8s_anywhere.rs index 7d028059..3e36ce77 100644 --- a/harmony_app/src/application/k8s_anywhere.rs +++ b/harmony_app/src/application/k8s_anywhere.rs @@ -361,9 +361,9 @@ impl Interpret for K8sAnywhereApplicationInterpret { )) })?; } + smoke_check_routes(&self.score.application.routes, &self.score.endpoints).await?; } - smoke_check_routes(&self.score.application.routes, &self.score.endpoints).await?; Ok(Outcome::success_with_details( format!("deployed application '{}'", self.score.application.name), vec![format!( @@ -499,7 +499,7 @@ fn lower( Ok(LoweredApplication { deployments, services, - ingress: ingress(application, endpoints), + ingress: ingress(application, endpoints)?, }) } @@ -803,14 +803,23 @@ fn k8s_service(service: &Service) -> K8sService { fn ingress( application: &Application, endpoints: &BTreeMap, -) -> Option { +) -> Result, AppError> { if application.routes.is_empty() { - return None; + return Ok(None); } let mut hosts = Vec::::new(); let mut paths = BTreeMap::>::new(); for route in &application.routes { - let host = endpoints.get(route.endpoint.name())?.host.clone(); + let host = endpoints + .get(route.endpoint.name()) + .ok_or_else(|| { + AppError::InvalidComposition(format!( + "unknown public endpoint '{}'", + route.endpoint.name() + )) + })? + .host + .clone(); if !paths.contains_key(&host) { hosts.push(host.clone()); } @@ -842,7 +851,7 @@ fn ingress( .into_iter() .collect(); let managed_tls = !tls_hosts.is_empty(); - Some(Ingress { + Ok(Some(Ingress { metadata: ObjectMeta { name: Some(application.name.clone()), annotations: managed_tls.then(|| { @@ -874,7 +883,7 @@ fn ingress( ..Default::default() }), ..Default::default() - }) + })) } /// Default window where startup probe failures do not restart the container. diff --git a/harmony_auth/src/backend.rs b/harmony_auth/src/backend.rs index a04a9f62..78bd992d 100644 --- a/harmony_auth/src/backend.rs +++ b/harmony_auth/src/backend.rs @@ -24,6 +24,7 @@ pub struct BackendAuth { zitadel_pat: String, openbao_url: String, openbao_token: String, + openbao_jwt_mount: String, } struct RoleRecord { @@ -61,9 +62,15 @@ impl BackendAuth { zitadel_pat, openbao_url: openbao_url.trim_end_matches('/').into(), openbao_token, + openbao_jwt_mount: "jwt".into(), }) } + pub fn with_openbao_jwt_mount(mut self, mount: impl Into) -> Self { + self.openbao_jwt_mount = mount.into(); + self + } + pub fn zitadel_url(&self) -> &str { &self.zitadel_url } @@ -527,6 +534,7 @@ impl BackendAuth { zitadel_pat: zitadel_pat.unwrap_or_else(|| self.zitadel_pat.clone()), openbao_url: self.openbao_url.clone(), openbao_token: openbao_token.unwrap_or_else(|| self.openbao_token.clone()), + openbao_jwt_mount: self.openbao_jwt_mount.clone(), } } @@ -591,8 +599,9 @@ impl BackendAuth { async fn roles(&self) -> Result, AuthError> { let mut roles = Vec::new(); - for name in self.role_names("jwt").await? { - if let Some(raw) = self.json(&format!("auth/jwt/role/{name}")).await? { + let mount = &self.openbao_jwt_mount; + for name in self.role_names(mount).await? { + if let Some(raw) = self.json(&format!("auth/{mount}/role/{name}")).await? { let subject_id = role_subject(&raw); if !subject_id.is_empty() { roles.push(RoleRecord { @@ -739,7 +748,7 @@ impl BackendAuth { raw.remove("policies"); self.openbao( Method::POST, - &format!("auth/jwt/role/{}", role.role.name), + &format!("auth/{}/role/{}", self.openbao_jwt_mount, role.role.name), Some(Value::Object(raw)), ) .await? diff --git a/harmony_auth_cli/src/main.rs b/harmony_auth_cli/src/main.rs index 2740c445..4665977c 100644 --- a/harmony_auth_cli/src/main.rs +++ b/harmony_auth_cli/src/main.rs @@ -380,7 +380,8 @@ async fn run(cli: &Cli) -> Result { config.openbao_url.clone(), config.openbao_token.clone(), ) - .map_err(AuthError::Backend)?; + .map_err(AuthError::Backend)? + .with_openbao_jwt_mount(cli.openbao_jwt_mount.clone()); match &cli.command { Command::Context { .. } => unreachable!("context configuration returned before dispatch"), diff --git a/harmony_config/src/lib.rs b/harmony_config/src/lib.rs index 3e66bde4..163001d8 100644 --- a/harmony_config/src/lib.rs +++ b/harmony_config/src/lib.rs @@ -501,31 +501,39 @@ pub async fn init_client(client: Arc) { *manager = Some(client); } +/// Global client: explicit [`init_client`], else lazy `HARMONY_SECRET_NAMESPACE` +/// (or `HARMONY_CONFIG_NAMESPACE`) via [`ConfigClient::for_namespace`]. +async fn global_client() -> Result, ConfigError> { + { + let manager = CONFIG_CLIENT.lock().await; + if let Some(client) = manager.as_ref() { + return Ok(client.clone()); + } + } + let namespace = std::env::var("HARMONY_SECRET_NAMESPACE") + .or_else(|_| std::env::var("HARMONY_CONFIG_NAMESPACE")) + .map_err(|_| { + ConfigError::EnvError( + "HARMONY_SECRET_NAMESPACE or HARMONY_CONFIG_NAMESPACE must be set \ + (project namespace for config/secrets)" + .into(), + ) + })?; + let client = Arc::new(ConfigClient::for_namespace(&namespace).await); + init_client(client.clone()).await; + Ok(client) +} + pub async fn get() -> Result { - let manager = CONFIG_CLIENT.lock().await; - manager - .as_ref() - .ok_or(ConfigError::NoSources)? - .get::() - .await + global_client().await?.get::().await } pub async fn get_or_prompt() -> Result { - let manager = CONFIG_CLIENT.lock().await; - manager - .as_ref() - .ok_or(ConfigError::NoSources)? - .get_or_prompt::() - .await + global_client().await?.get_or_prompt::().await } pub async fn set(config: &T) -> Result<(), ConfigError> { - let manager = CONFIG_CLIENT.lock().await; - manager - .as_ref() - .ok_or(ConfigError::NoSources)? - .set::(config) - .await + global_client().await?.set::(config).await } pub fn default_config_dir() -> Option { diff --git a/harmony_config/src/source/env.rs b/harmony_config/src/source/env.rs index 259711e3..0516d30c 100644 --- a/harmony_config/src/source/env.rs +++ b/harmony_config/src/source/env.rs @@ -1,6 +1,6 @@ use crate::{ConfigClass, ConfigError, ConfigSource}; use async_trait::async_trait; -use log::{debug, info}; +use log::debug; pub struct EnvSource; diff --git a/harmony_secret/src/lib.rs b/harmony_secret/src/lib.rs index 1b6a5825..3dcf5223 100644 --- a/harmony_secret/src/lib.rs +++ b/harmony_secret/src/lib.rs @@ -3,46 +3,22 @@ mod deployment_grants; mod openbao_policy; pub mod store; -use crate::config::SECRET_NAMESPACE; use async_trait::async_trait; -use config::HARMONY_SSO_CLIENT_ID; -use config::HARMONY_SSO_URL; -use config::INFISICAL_CLIENT_ID; -use config::INFISICAL_CLIENT_SECRET; -use config::INFISICAL_ENVIRONMENT; -use config::INFISICAL_PROJECT_ID; -use config::INFISICAL_URL; -use config::OPENBAO_AUTH_MOUNT; -use config::OPENBAO_KV_MOUNT; -use config::OPENBAO_PASSWORD; -use config::OPENBAO_SKIP_TLS; -use config::OPENBAO_TOKEN; -use config::OPENBAO_URL; -use config::OPENBAO_USERNAME; -use config::SECRET_STORE; use interactive_parse::InteractiveParseObj; -use log::debug; -use log::info; use schemars::JsonSchema; use serde::{Serialize, de::DeserializeOwned}; use std::fmt; -use store::InfisicalSecretStore; -use store::LocalFileSecretStore; pub use store::{OpenbaoSecretStore, OpenbaoStoreOptions, ZitadelJwtBearerConfig}; use thiserror::Error; -use tokio::sync::OnceCell; pub use deployment_grants::OpenBaoDeploymentSecretGrants; pub use harmony_secret_derive::Secret; pub use openbao_policy::{HARMONY_STATE_SUBPATH, OpenBaoPolicyManager, render_tenant_policy}; -// The Secret trait remains the same. -// pub trait Secret: Serialize + DeserializeOwned + Sized { pub trait Secret: Serialize + DeserializeOwned + JsonSchema + InteractiveParseObj + Sized { const KEY: &'static str; } -// The error enum remains the same. #[derive(Debug, Error)] pub enum SecretStoreError { #[error("Secret not found for key '{key}' in namespace '{namespace}'")] @@ -61,7 +37,6 @@ pub enum SecretStoreError { Store(#[from] Box), } -// The trait is now async! #[async_trait] pub trait SecretStore: fmt::Debug + Send + Sync { async fn get_raw(&self, namespace: &str, key: &str) -> Result, SecretStoreError>; @@ -72,173 +47,3 @@ pub trait SecretStore: fmt::Debug + Send + Sync { value: &[u8], ) -> Result<(), SecretStoreError>; } - -// Use OnceCell for async-friendly, one-time initialization. -static SECRET_MANAGER: OnceCell = OnceCell::const_new(); - -/// Initializes and returns a reference to the global SecretManager. -async fn get_secret_manager() -> &'static SecretManager { - SECRET_MANAGER.get_or_init(init_secret_manager).await -} - -/// The async initialization function for the SecretManager. -async fn init_secret_manager() -> SecretManager { - let default_secret_store = "infisical".to_string(); - let store_type = SECRET_STORE.as_ref().unwrap_or(&default_secret_store); - - let store: Box = match store_type.as_str() { - "file" => Box::new(LocalFileSecretStore), - "openbao" | "vault" => { - let store = OpenbaoSecretStore::new(OpenbaoStoreOptions { - base_url: OPENBAO_URL.clone().expect("Openbao/Vault URL must be set, see harmony_secret config for ways to provide it. You can try with OPENBAO_URL or VAULT_ADDR"), - kv_mount: OPENBAO_KV_MOUNT.clone(), - auth_mount: OPENBAO_AUTH_MOUNT.clone(), - skip_tls: *OPENBAO_SKIP_TLS, - token: OPENBAO_TOKEN.clone(), - username: OPENBAO_USERNAME.clone(), - password: OPENBAO_PASSWORD.clone(), - zitadel_sso_url: HARMONY_SSO_URL.clone(), - zitadel_client_id: HARMONY_SSO_CLIENT_ID.clone(), - jwt_role: None, - jwt_auth_mount: None, - zitadel_jwt_bearer: None, - }) - .await - .expect("Failed to initialize Openbao/Vault secret store"); - Box::new(store) - } - "infisical" => { - let store = InfisicalSecretStore::new( - INFISICAL_URL.clone().expect("Infisical url must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_URL"), - INFISICAL_PROJECT_ID.clone().expect("Infisical project id must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_PROJECT_ID"), - INFISICAL_ENVIRONMENT.clone().expect("Infisical environment must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_ENVIRONMENT"), - INFISICAL_CLIENT_ID.clone().expect("Infisical client id must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_CLIENT_ID"), - INFISICAL_CLIENT_SECRET.clone().expect("Infisical client secret must be set, see harmony_secret config for ways to provide it. You can try with HARMONY_SECRET_INFISICAL_CLIENT_SECRET"), - ) - .await - .expect("Failed to initialize Infisical secret store"); - Box::new(store) - } - other => panic!( - "HARMONY_SECRET_STORE='{other}' is not a recognized store type. \ - Valid values: 'file', 'openbao' (or 'vault'), 'infisical'." - ), - }; - - SecretManager::new(SECRET_NAMESPACE.clone(), store) -} - -/// Manages the lifecycle of secrets, providing a simple static API. -#[deprecated( - note = "Use harmony_config::ConfigClient instead; it unifies config + secrets over the same stores." -)] -#[derive(Debug)] -pub struct SecretManager { - namespace: String, - store: Box, -} - -impl SecretManager { - fn new(namespace: String, store: Box) -> Self { - Self { namespace, store } - } - - /// Retrieves and deserializes a secret. - pub async fn get() -> Result { - let manager = get_secret_manager().await; - debug!("Getting secret ns {} key {}", &manager.namespace, T::KEY); - let raw_value = manager.store.get_raw(&manager.namespace, T::KEY).await?; - serde_json::from_slice(&raw_value).map_err(|e| SecretStoreError::Deserialization { - key: T::KEY.to_string(), - source: e, - }) - } - - pub async fn get_or_prompt() -> Result { - let secret = Self::get::().await; - let manager = get_secret_manager().await; - let prompted = secret.is_err(); - - let secret = secret.or_else(|e| -> Result { - debug!("Could not get secret : {e}"); - - let ns = &manager.namespace; - let key = T::KEY; - - debug!("Prompting interactively for secret {ns} {key}"); - info!("Secret not found for {} {}, fill the fields :", ns, key); - - T::parse_to_obj().map_err(|e| { - SecretStoreError::Store( - format!("Failed to interactively parse secret {ns} {key}: {e}").into(), - ) - }) - })?; - - if prompted { - Self::set(&secret).await?; - } - - Ok(secret) - } - - /// Serializes and stores a secret. - pub async fn set(secret: &T) -> Result<(), SecretStoreError> { - let manager = get_secret_manager().await; - let raw_value = - serde_json::to_vec(secret).map_err(|e| SecretStoreError::Serialization { - key: T::KEY.to_string(), - source: e, - })?; - manager - .store - .set_raw(&manager.namespace, T::KEY, &raw_value) - .await - } -} - -// The integration test below requires a live OpenBao instance and is -// deliberately gated behind a custom cfg flag so it doesn't run in the -// regular `cargo test` flow. Enable with -// `RUSTFLAGS="--cfg secrete2etest" cargo test`. -#[cfg(test)] -#[cfg(secrete2etest)] -mod test { - use super::*; - use pretty_assertions::assert_eq; - use serde::{Deserialize, Serialize}; - - #[derive(Serialize, Deserialize, JsonSchema, Debug, PartialEq)] - struct TestUserMeta { - labels: Vec, - } - - #[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] - struct TestSecret { - user: String, - password: String, - metadata: TestUserMeta, - } - - #[tokio::test] - async fn set_and_retrieve_secret() { - let secret = TestSecret { - user: String::from("user"), - password: String::from("password"), - metadata: TestUserMeta { - labels: vec![ - String::from("label1"), - String::from("label2"), - String::from( - "some longet label with \" special @#%$)(udiojcia[]]] \"'asdij'' characters Nдs はにほへとちり าฟันพัฒนา yağız şoföre ç � � � � � � � � � � � � � 👩‍👩‍👧‍👦 /span> 👩‍👧‍👦 and why not emojis ", - ), - ], - }, - }; - - SecretManager::set(&secret).await.unwrap(); - let value = SecretManager::get::().await.unwrap(); - - assert_eq!(value, secret); - } -}