chore: clear compile warnings; retire SecretManager #346

Merged
johnride merged 3 commits from chore/clear-compile-warnings into fix/app-deploy-safety 2026-08-08 23:59:17 +00:00
39 changed files with 119 additions and 306 deletions

17
Cargo.lock generated
View File

@@ -1313,7 +1313,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"env_logger", "env_logger",
"harmony_secret", "harmony_config",
"harmony_types", "harmony_types",
"log", "log",
"regex", "regex",
@@ -3181,9 +3181,8 @@ dependencies = [
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
"harmony_config",
"harmony_macros", "harmony_macros",
"harmony_secret",
"harmony_secret_derive",
"harmony_types", "harmony_types",
"log", "log",
"schemars 0.8.22", "schemars 0.8.22",
@@ -3240,8 +3239,8 @@ dependencies = [
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
"harmony_config",
"harmony_macros", "harmony_macros",
"harmony_secret",
"harmony_types", "harmony_types",
"log", "log",
"schemars 0.8.22", "schemars 0.8.22",
@@ -3320,9 +3319,8 @@ dependencies = [
"env_logger", "env_logger",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
"harmony_config",
"harmony_macros", "harmony_macros",
"harmony_secret",
"harmony_secret_derive",
"harmony_types", "harmony_types",
"log", "log",
"schemars 0.8.22", "schemars 0.8.22",
@@ -3465,7 +3463,7 @@ dependencies = [
"harmony", "harmony",
"harmony-fleet-deploy", "harmony-fleet-deploy",
"harmony_cli", "harmony_cli",
"harmony_secret", "harmony_config",
"harmony_types", "harmony_types",
"log", "log",
"reqwest 0.12.28", "reqwest 0.12.28",
@@ -6445,9 +6443,8 @@ dependencies = [
"cidr", "cidr",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
"harmony_config",
"harmony_macros", "harmony_macros",
"harmony_secret",
"harmony_secret_derive",
"harmony_types", "harmony_types",
"schemars 0.8.22", "schemars 0.8.22",
"serde", "serde",
@@ -8896,8 +8893,8 @@ dependencies = [
"cidr", "cidr",
"harmony", "harmony",
"harmony_cli", "harmony_cli",
"harmony_config",
"harmony_macros", "harmony_macros",
"harmony_secret",
"tokio", "tokio",
] ]

View File

@@ -1,4 +1,4 @@
FROM docker.io/rust:1.94 AS build FROM docker.io/rust:1.97 AS build
WORKDIR /app WORKDIR /app

View File

@@ -14,6 +14,6 @@ tokio.workspace = true
log.workspace = true log.workspace = true
env_logger.workspace = true env_logger.workspace = true
regex = "1.11.3" regex = "1.11.3"
harmony_secret = { path = "../harmony_secret" } harmony_config = { path = "../harmony_config" }
serde.workspace = true serde.workspace = true
schemars = "0.8" schemars = "0.8"

View File

@@ -1,12 +1,13 @@
use std::net::{IpAddr, Ipv4Addr}; use std::net::{IpAddr, Ipv4Addr};
use brocade::{BrocadeOptions, ssh}; use brocade::{BrocadeOptions, ssh};
use harmony_secret::{Secret, SecretManager}; use harmony_config::Config;
use harmony_types::switch::PortLocation; use harmony_types::switch::PortLocation;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Secret, Clone, Debug, JsonSchema, Serialize, Deserialize)] #[derive(Config, Clone, Debug, JsonSchema, Serialize, Deserialize)]
#[config(secret)]
struct BrocadeSwitchAuth { struct BrocadeSwitchAuth {
username: String, username: String,
password: String, password: String,
@@ -21,7 +22,7 @@ async fn main() {
// let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st // let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 4, 11)); // brocade @ st
let switch_addresses = vec![ip]; let switch_addresses = vec![ip];
let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>() let config = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
.await .await
.unwrap(); .unwrap();

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use std::{path::PathBuf, str::FromStr, sync::Arc}; use std::{path::PathBuf, str::FromStr, sync::Arc};
use harmony::{ use harmony::{

View File

@@ -12,7 +12,7 @@ path = "src/main.rs"
harmony = { path = "../../harmony" } harmony = { path = "../../harmony" }
harmony-fleet-deploy = { path = "../../fleet/harmony-fleet-deploy" } harmony-fleet-deploy = { path = "../../fleet/harmony-fleet-deploy" }
harmony_cli = { path = "../../harmony_cli" } harmony_cli = { path = "../../harmony_cli" }
harmony_secret = { path = "../../harmony_secret" } harmony_config = { path = "../../harmony_config" }
harmony_types = { path = "../../harmony_types" } harmony_types = { path = "../../harmony_types" }
tokio.workspace = true tokio.workspace = true
log.workspace = true log.workspace = true

View File

@@ -23,7 +23,7 @@
//! - SSH server enabled //! - SSH server enabled
//! - An admin user with sudo. Passwordless sudo is detected and //! - An admin user with sudo. Passwordless sudo is detected and
//! used silently; otherwise the example prompts for a sudo //! 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 //! - Your driver-machine SSH public key in that user's
//! `~/.ssh/authorized_keys` //! `~/.ssh/authorized_keys`
//! //!
@@ -39,7 +39,6 @@ use harmony::config::secret::SudoPassword;
use harmony::inventory::Inventory; use harmony::inventory::Inventory;
use harmony::modules::linux::{LinuxHostTopology, SshCredentials, ensure_ansible_venv, ssh_exec}; use harmony::modules::linux::{LinuxHostTopology, SshCredentials, ensure_ansible_venv, ssh_exec};
use harmony_fleet_deploy::{FleetDeviceAuth, FleetDeviceSetupConfig, FleetDeviceSetupScore}; use harmony_fleet_deploy::{FleetDeviceAuth, FleetDeviceSetupConfig, FleetDeviceSetupScore};
use harmony_secret::SecretManager;
use harmony_types::id::Id; use harmony_types::id::Id;
use log::info; use log::info;
use std::path::PathBuf; use std::path::PathBuf;
@@ -144,8 +143,8 @@ async fn main() -> Result<()> {
}; };
// If the Pi doesn't have passwordless sudo, fetch the password // If the Pi doesn't have passwordless sudo, fetch the password
// through SecretManager (same flow other scores use for SSH keys // through harmony_config (same flow other scores use for SSH keys
// etc. — see harmony_secret/src/lib.rs:145). First run prompts; // etc.). First run prompts;
// subsequent runs reuse the cached value. Probe with `sudo -n` // subsequent runs reuse the cached value. Probe with `sudo -n`
// first so we don't prompt the operator for a password they // first so we don't prompt the operator for a password they
// don't need. // don't need.
@@ -154,7 +153,7 @@ async fn main() -> Result<()> {
.map_err(|e| anyhow::anyhow!("sudo probe: {e}"))?; .map_err(|e| anyhow::anyhow!("sudo probe: {e}"))?;
if probe.rc != 0 { if probe.rc != 0 {
info!("device requires a sudo password — fetching from secret store"); info!("device requires a sudo password — fetching from secret store");
let secret = SecretManager::get_or_prompt::<SudoPassword>() let secret = harmony_config::get_or_prompt::<SudoPassword>()
.await .await
.map_err(|e| anyhow::anyhow!("get sudo password: {e}"))?; .map_err(|e| anyhow::anyhow!("get sudo password: {e}"))?;
creds.sudo_password = Some(secret.password); creds.sudo_password = Some(secret.password);

View File

@@ -11,8 +11,7 @@ harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" } harmony_cli = { path = "../../harmony_cli" }
harmony_macros = { path = "../../harmony_macros" } harmony_macros = { path = "../../harmony_macros" }
harmony_types = { path = "../../harmony_types" } harmony_types = { path = "../../harmony_types" }
harmony_secret = { path = "../../harmony_secret" } harmony_config = { path = "../../harmony_config" }
harmony_secret_derive = { path = "../../harmony_secret_derive" }
tokio.workspace = true tokio.workspace = true
cidr.workspace = true cidr.workspace = true
serde = { workspace = true } serde = { workspace = true }

View File

@@ -9,8 +9,8 @@ use harmony::{
inventory::Inventory, inventory::Inventory,
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
}; };
use harmony_config::Config;
use harmony_macros::{ip, ipv4}; use harmony_macros::{ip, ipv4};
use harmony_secret::{Secret, SecretManager};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@@ -18,7 +18,8 @@ use std::{
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
}; };
#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] #[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
#[config(secret)]
struct OkdAddNodeFirewallConfig { struct OkdAddNodeFirewallConfig {
username: String, username: String,
password: String, password: String,
@@ -41,7 +42,7 @@ pub async fn get_topology() -> HAClusterTopology {
.expect("Failed to connect to switch"), .expect("Failed to connect to switch"),
); );
let config = SecretManager::get_or_prompt::<OkdAddNodeFirewallConfig>() let config = harmony_config::get_or_prompt::<OkdAddNodeFirewallConfig>()
.await .await
.unwrap(); .unwrap();
let api_creds = harmony::config::secret::OPNSenseApiCredentials { let api_creds = harmony::config::secret::OPNSenseApiCredentials {

View File

@@ -10,8 +10,7 @@ publish = false
harmony = { path = "../../harmony" } harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" } harmony_cli = { path = "../../harmony_cli" }
harmony_types = { path = "../../harmony_types" } harmony_types = { path = "../../harmony_types" }
harmony_secret = { path = "../../harmony_secret" } harmony_config = { path = "../../harmony_config" }
harmony_secret_derive = { path = "../../harmony_secret_derive" }
cidr = { workspace = true } cidr = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
harmony_macros = { path = "../../harmony_macros" } harmony_macros = { path = "../../harmony_macros" }

View File

@@ -14,14 +14,13 @@ use harmony::{
score::Score, score::Score,
topology::HAClusterTopology, topology::HAClusterTopology,
}; };
use harmony_secret::SecretManager;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let inventory = get_inventory(); let inventory = get_inventory();
let topology = get_topology().await; let topology = get_topology().await;
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap(); let ssh_key = harmony_config::get_or_prompt::<SshKeyPair>().await.unwrap();
let mut scores: Vec<Box<dyn Score<HAClusterTopology>>> = vec![Box::new(OKDIpxeScore { let mut scores: Vec<Box<dyn Score<HAClusterTopology>>> = vec![Box::new(OKDIpxeScore {
kickstart_filename: "inventory.kickstart".to_string(), kickstart_filename: "inventory.kickstart".to_string(),

View File

@@ -12,7 +12,6 @@ use harmony::{
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
}; };
use harmony_macros::{ip, ipv4}; use harmony_macros::{ip, ipv4};
use harmony_secret::SecretManager;
use std::{ use std::{
net::IpAddr, net::IpAddr,
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
@@ -24,7 +23,7 @@ pub async fn get_topology() -> HAClusterTopology {
name: String::from("opnsense-1"), name: String::from("opnsense-1"),
}; };
let switch_auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>() let switch_auth = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
.await .await
.expect("Failed to get credentials"); .expect("Failed to get credentials");
@@ -43,10 +42,10 @@ pub async fn get_topology() -> HAClusterTopology {
let switch_client = Arc::new(switch_client); let switch_client = Arc::new(switch_client);
let ssh_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>() let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
.await .await
.unwrap(); .unwrap();
let api_creds = SecretManager::get_or_prompt::<OPNSenseApiCredentials>() let api_creds = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
.await .await
.unwrap(); .unwrap();

View File

@@ -10,8 +10,7 @@ publish = false
harmony = { path = "../../harmony" } harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" } harmony_cli = { path = "../../harmony_cli" }
harmony_types = { path = "../../harmony_types" } harmony_types = { path = "../../harmony_types" }
harmony_secret = { path = "../../harmony_secret" } harmony_config = { path = "../../harmony_config" }
harmony_secret_derive = { path = "../../harmony_secret_derive" }
cidr = { workspace = true } cidr = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
harmony_macros = { path = "../../harmony_macros" } harmony_macros = { path = "../../harmony_macros" }

View File

@@ -6,7 +6,6 @@ use harmony::{
data::{FileContent, FilePath}, data::{FileContent, FilePath},
modules::okd::ipxe::OKDIpxeScore, modules::okd::ipxe::OKDIpxeScore,
}; };
use harmony_secret::SecretManager;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -15,7 +14,7 @@ async fn main() {
let kickstart_filename = "inventory.kickstart".to_string(); let kickstart_filename = "inventory.kickstart".to_string();
let harmony_inventory_agent = "harmony_inventory_agent".to_string(); let harmony_inventory_agent = "harmony_inventory_agent".to_string();
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap(); let ssh_key = harmony_config::get_or_prompt::<SshKeyPair>().await.unwrap();
let ipxe_score = OKDIpxeScore { let ipxe_score = OKDIpxeScore {
kickstart_filename, kickstart_filename,

View File

@@ -12,7 +12,6 @@ use harmony::{
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
}; };
use harmony_macros::{ip, ipv4}; use harmony_macros::{ip, ipv4};
use harmony_secret::SecretManager;
use std::{ use std::{
net::IpAddr, net::IpAddr,
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
@@ -24,7 +23,7 @@ pub async fn get_topology() -> HAClusterTopology {
name: String::from("opnsense-1"), name: String::from("opnsense-1"),
}; };
let switch_auth = SecretManager::get_or_prompt::<BrocadeSwitchAuth>() let switch_auth = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
.await .await
.expect("Failed to get credentials"); .expect("Failed to get credentials");
@@ -43,11 +42,11 @@ pub async fn get_topology() -> HAClusterTopology {
let switch_client = Arc::new(switch_client); let switch_client = Arc::new(switch_client);
let ssh_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>() let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
.await .await
.unwrap(); .unwrap();
let api_creds = let api_creds =
SecretManager::get_or_prompt::<harmony::config::secret::OPNSenseApiCredentials>() harmony_config::get_or_prompt::<harmony::config::secret::OPNSenseApiCredentials>()
.await .await
.unwrap(); .unwrap();

View File

@@ -16,7 +16,7 @@ harmony_macros = { path = "../../harmony_macros" }
log = { workspace = true } log = { workspace = true }
env_logger = { workspace = true } env_logger = { workspace = true }
url = { workspace = true } url = { workspace = true }
harmony_secret = { path = "../../harmony_secret" } harmony_config = { path = "../../harmony_config" }
brocade = { path = "../../brocade" } brocade = { path = "../../brocade" }
serde = { workspace = true } serde = { workspace = true }
schemars = "0.8" schemars = "0.8"

View File

@@ -6,9 +6,6 @@ use harmony::{
topology::LogicalHost, topology::LogicalHost,
}; };
use harmony_macros::{ip, ipv4}; use harmony_macros::{ip, ipv4};
use harmony_secret::{Secret, SecretManager};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -17,10 +14,10 @@ async fn main() {
name: String::from("opnsense-1"), name: String::from("opnsense-1"),
}; };
let ssh_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>() let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
.await .await
.expect("Failed to get SSH credentials"); .expect("Failed to get SSH credentials");
let api_creds = SecretManager::get_or_prompt::<OPNSenseApiCredentials>() let api_creds = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
.await .await
.expect("Failed to get API credentials"); .expect("Failed to get API credentials");
@@ -67,9 +64,3 @@ async fn main() {
.await .await
.unwrap(); .unwrap();
} }
#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug)]
pub struct BrocadeSwitchAuth {
pub username: String,
pub password: String,
}

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use harmony::{ use harmony::{

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use std::{path::PathBuf, sync::Arc}; use std::{path::PathBuf, sync::Arc};
use harmony::{ use harmony::{

View File

@@ -10,6 +10,6 @@ publish = false
harmony = { path = "../../harmony" } harmony = { path = "../../harmony" }
harmony_cli = { path = "../../harmony_cli" } harmony_cli = { path = "../../harmony_cli" }
harmony_macros = { path = "../../harmony_macros" } harmony_macros = { path = "../../harmony_macros" }
harmony_secret = { path = "../../harmony_secret" } harmony_config = { path = "../../harmony_config" }
cidr = { workspace = true } cidr = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }

View File

@@ -16,7 +16,6 @@ use harmony::{
topology::HAClusterTopology, topology::HAClusterTopology,
}; };
use harmony_macros::cidrv4; use harmony_macros::cidrv4;
use harmony_secret::SecretManager;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
@@ -25,7 +24,7 @@ async fn main() {
let inventory = get_inventory(); let inventory = get_inventory();
let topology = get_topology().await; let topology = get_topology().await;
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await.unwrap(); let ssh_key = harmony_config::get_or_prompt::<SshKeyPair>().await.unwrap();
// Discovery runs as a CIDR scan across the sttest LAN on the // Discovery runs as a CIDR scan across the sttest LAN on the
// harmony_inventory_agent's default port. Shared between the install // harmony_inventory_agent's default port. Shared between the install

View File

@@ -7,7 +7,6 @@ use harmony::{
topology::{HAClusterTopology, LogicalHost, UnmanagedRouter}, topology::{HAClusterTopology, LogicalHost, UnmanagedRouter},
}; };
use harmony_macros::{ip, ipv4}; use harmony_macros::{ip, ipv4};
use harmony_secret::SecretManager;
use std::{ use std::{
net::IpAddr, net::IpAddr,
sync::{Arc, OnceLock}, sync::{Arc, OnceLock},
@@ -22,10 +21,10 @@ pub async fn get_opnsense() -> Arc<harmony::infra::opnsense::OPNSenseFirewall> {
name: String::from("fw0"), name: String::from("fw0"),
}; };
let ssh_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>() let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
.await .await
.unwrap(); .unwrap();
let api_credentials = SecretManager::get_or_prompt::<OPNSenseApiCredentials>() let api_credentials = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
.await .await
.unwrap(); .unwrap();

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{

View File

@@ -33,7 +33,7 @@ impl K8sClient {
.take(6); .take(6);
let attempt = Mutex::new(0u32); 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; let mut n = attempt.lock().await;
*n += 1; *n += 1;
debug!("Running Kubernetes API discovery (attempt {})", *n); debug!("Running Kubernetes API discovery (attempt {})", *n);

View File

@@ -1,27 +1,31 @@
use harmony_secret_derive::Secret; use harmony_config::Config;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] #[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
#[config(secret)]
pub struct OPNSenseFirewallCredentials { pub struct OPNSenseFirewallCredentials {
pub username: String, pub username: String,
pub password: String, pub password: String,
} }
#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] #[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
#[config(secret)]
pub struct OPNSenseApiCredentials { pub struct OPNSenseApiCredentials {
pub key: String, pub key: String,
pub secret: String, pub secret: String,
} }
// TODO we need a better way to handle multiple "instances" of the same secret structure. // 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 struct SshKeyPair {
pub private: String, pub private: String,
pub public: String, pub public: String,
} }
#[derive(Secret, Serialize, Deserialize, JsonSchema, Debug, PartialEq)] #[derive(Config, Serialize, Deserialize, JsonSchema, Debug, PartialEq)]
#[config(secret)]
pub struct RedhatSecret { pub struct RedhatSecret {
pub pull_secret: String, pub pull_secret: String,
} }
@@ -33,7 +37,8 @@ pub struct RedhatSecret {
/// Not used for SSH login itself — `LinuxHostTopology` still requires /// Not used for SSH login itself — `LinuxHostTopology` still requires
/// a `private_key_path` for that. SSH password auth is a possible /// a `private_key_path` for that. SSH password auth is a possible
/// future extension; see the TODO on `SshCredentials`. /// 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 struct SudoPassword {
pub password: String, pub password: String,
} }

View File

@@ -35,8 +35,6 @@ use crate::topology::{
Topology, Topology,
}; };
use harmony_secret::SecretManager;
// ── FirewallPairTopology ─────────────────────────────────────────── // ── FirewallPairTopology ───────────────────────────────────────────
/// An OPNsense HA firewall pair managed via CARP. /// An OPNsense HA firewall pair managed via CARP.
@@ -58,16 +56,16 @@ impl FirewallPairTopology {
/// - `OPNSENSE_BACKUP_IP` — IP address of the backup firewall /// - `OPNSENSE_BACKUP_IP` — IP address of the backup firewall
/// - `OPNSENSE_API_PORT` — API/web GUI port (default: 443) /// - `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 { pub async fn opnsense_from_config() -> Self {
// TODO: both firewalls share the same credentials. Once named config // TODO: both firewalls share the same credentials. Once named config
// instances are available (ROADMAP/11), use per-device credentials: // instances are available (ROADMAP/11), use per-device credentials:
// ConfigClient::get_named::<OPNSenseApiCredentials>("fw-primary") // ConfigClient::get_named::<OPNSenseApiCredentials>("fw-primary")
let ssh_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>() let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
.await .await
.expect("Failed to get SSH credentials"); .expect("Failed to get SSH credentials");
let api_creds = SecretManager::get_or_prompt::<OPNSenseApiCredentials>() let api_creds = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
.await .await
.expect("Failed to get API credentials"); .expect("Failed to get API credentials");

View File

@@ -9,6 +9,7 @@
use harmony_types::firewall::LaggProtocol; use harmony_types::firewall::LaggProtocol;
/// NetworkManager bond `mode=` value for a `LaggProtocol`. /// NetworkManager bond `mode=` value for a `LaggProtocol`.
#[cfg(test)]
pub(crate) fn nm_bond_mode(proto: &LaggProtocol) -> &'static str { pub(crate) fn nm_bond_mode(proto: &LaggProtocol) -> &'static str {
match proto { match proto {
LaggProtocol::Lacp => "802.3ad", LaggProtocol::Lacp => "802.3ad",

View File

@@ -27,8 +27,7 @@ use crate::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use harmony_secret::SecretManager; use harmony_config::Config;
use harmony_secret_derive::Secret;
use harmony_types::net::Url; use harmony_types::net::Url;
use kube::api::ObjectMeta; use kube::api::ObjectMeta;
use log::{debug, info}; use log::{debug, info};
@@ -100,7 +99,7 @@ impl<
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let config = SecretManager::get_or_prompt::<NtfyAuth>().await.unwrap(); let config = harmony_config::get_or_prompt::<NtfyAuth>().await.unwrap();
let ntfy_default_auth_header = format!( let ntfy_default_auth_header = format!(
"Basic {}", "Basic {}",
@@ -143,7 +142,8 @@ impl<
} }
} }
#[derive(Secret, Serialize, Deserialize, JsonSchema, Clone, Debug)] #[derive(Config, Serialize, Deserialize, JsonSchema, Clone, Debug)]
#[config(secret)]
struct NtfyAuth { struct NtfyAuth {
username: String, username: String,
password: String, password: String,

View File

@@ -2,7 +2,7 @@ use std::net::IpAddr;
use async_trait::async_trait; use async_trait::async_trait;
use brocade::BrocadeOptions; use brocade::BrocadeOptions;
use harmony_secret::{Secret, SecretManager}; use harmony_config::Config;
use harmony_types::id::Id; use harmony_types::id::Id;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -38,7 +38,8 @@ pub struct BrocadeEnableSnmpInterpret {
score: BrocadeEnableSnmpScore, score: BrocadeEnableSnmpScore,
} }
#[derive(Secret, Clone, Debug, JsonSchema, Serialize, Deserialize)] #[derive(Config, Clone, Debug, JsonSchema, Serialize, Deserialize)]
#[config(secret)]
pub struct BrocadeSwitchAuth { pub struct BrocadeSwitchAuth {
pub username: String, pub username: String,
pub password: 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 struct BrocadeSnmpAuth {
pub username: String, pub username: String,
pub auth_password: String, pub auth_password: String,
@@ -66,11 +68,11 @@ impl<T: Topology> Interpret<T> for BrocadeEnableSnmpInterpret {
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let switch_addresses = &self.score.switch_ips; let switch_addresses = &self.score.switch_ips;
let snmp_auth = SecretManager::get_or_prompt::<BrocadeSnmpAuth>() let snmp_auth = harmony_config::get_or_prompt::<BrocadeSnmpAuth>()
.await .await
.unwrap(); .unwrap();
let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>() let config = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
.await .await
.unwrap(); .unwrap();

View File

@@ -78,7 +78,7 @@ where
.max_delay(Duration::from_millis(1000)) .max_delay(Duration::from_millis(1000))
.take(10); .take(10);
Retry::spawn(strategy, || async { Retry::start(strategy, || async {
log::debug!("Attempting CA cert fetch"); log::debug!("Attempting CA cert fetch");
let res = self let res = self

View File

@@ -1,8 +1,8 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use async_trait::async_trait; use async_trait::async_trait;
use harmony_config::Config;
use harmony_k8s::KubernetesDistribution; use harmony_k8s::KubernetesDistribution;
use harmony_secret::{Secret, SecretManager};
use harmony_types::id::Id; use harmony_types::id::Id;
use k8s_openapi::{ByteString, api::core::v1::Secret as K8sSecret}; use k8s_openapi::{ByteString, api::core::v1::Secret as K8sSecret};
use kube::api::ObjectMeta; use kube::api::ObjectMeta;
@@ -344,7 +344,7 @@ impl NatsK8sInterpret {
peers: Option<Vec<NatsCluster>>, peers: Option<Vec<NatsCluster>>,
namespace: String, namespace: String,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let admin = SecretManager::get_or_prompt::<NatsAdmin>().await.unwrap(); let admin = harmony_config::get_or_prompt::<NatsAdmin>().await.unwrap();
let admin_user = admin.user.clone(); let admin_user = admin.user.clone();
let admin_password = admin.password.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 { struct NatsAdmin {
user: String, user: String,
password: String, password: String,

View File

@@ -27,7 +27,7 @@ use harmony_k8s::{
K8sClient, K8sClient,
csr::{KUBE_APISERVER_CLIENT_KUBELET_SIGNER, KUBELET_SERVING_SIGNER}, csr::{KUBE_APISERVER_CLIENT_KUBELET_SIGNER, KUBELET_SERVING_SIGNER},
}; };
use harmony_secret::SecretManager;
use harmony_types::{id::Id, ssh::SshCredentials}; use harmony_types::{id::Id, ssh::SshCredentials};
use k8s_openapi::api::core::v1::{Node, Secret}; use k8s_openapi::api::core::v1::{Node, Secret};
use kube::api::ListParams; use kube::api::ListParams;
@@ -349,7 +349,9 @@ impl Interpret<HAClusterTopology> for OKDAddNodeInterpret {
.map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?; .map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
if rebooted { if rebooted {
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await?; let ssh_key = harmony_config::get_or_prompt::<SshKeyPair>()
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
let creds = SshCredentials::SshKey { let creds = SshCredentials::SshKey {
username: "root".to_string(), username: "root".to_string(),
private_pem: ssh_key.private, private_pem: ssh_key.private,

View File

@@ -18,7 +18,7 @@ use crate::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use derive_new::new; use derive_new::new;
use harmony_secret::SecretManager;
use harmony_types::id::Id; use harmony_types::id::Id;
use harmony_types::net::Url; use harmony_types::net::Url;
use log::{debug, info}; use log::{debug, info};
@@ -115,8 +115,12 @@ impl OKDSetup02BootstrapInterpret {
); );
} }
let redhat_secret = SecretManager::get_or_prompt::<RedhatSecret>().await?; let redhat_secret = harmony_config::get_or_prompt::<RedhatSecret>()
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await?; .await
.map_err(|e| InterpretError::new(e.to_string()))?;
let ssh_key = harmony_config::get_or_prompt::<SshKeyPair>()
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
let install_config_yaml = InstallConfigYaml { let install_config_yaml = InstallConfigYaml {
cluster_name: &topology.get_cluster_name(), cluster_name: &topology.get_cluster_name(),

View File

@@ -19,7 +19,6 @@
//! regardless of whether the stable name is a regular file or a symlink. //! regardless of whether the stable name is a regular file or a symlink.
use async_trait::async_trait; use async_trait::async_trait;
use harmony_secret::SecretManager;
use harmony_types::{id::Id, ssh::SshCredentials}; use harmony_types::{id::Id, ssh::SshCredentials};
use log::{debug, info}; use log::{debug, info};
use serde::Serialize; use serde::Serialize;
@@ -71,7 +70,9 @@ impl Interpret<HAClusterTopology> for OKDOsArtifactsInterpret {
topology: &HAClusterTopology, topology: &HAClusterTopology,
) -> Result<Outcome, InterpretError> { ) -> Result<Outcome, InterpretError> {
let fw_ip = topology.firewall.get_ip(); let fw_ip = topology.firewall.get_ip();
let fw_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>().await?; let fw_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
.await
.map_err(|e| InterpretError::new(e.to_string()))?;
let ssh_creds = SshCredentials::Password { let ssh_creds = SshCredentials::Password {
username: fw_creds.username, username: fw_creds.username,
password: fw_creds.password, password: fw_creds.password,

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use harmony::{ use harmony::{
inventory::Inventory, inventory::Inventory,
modules::{ modules::{

View File

@@ -501,31 +501,39 @@ pub async fn init_client(client: Arc<ConfigClient>) {
*manager = Some(client); *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<Arc<ConfigClient>, 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<T: Config>() -> Result<T, ConfigError> { pub async fn get<T: Config>() -> Result<T, ConfigError> {
let manager = CONFIG_CLIENT.lock().await; global_client().await?.get::<T>().await
manager
.as_ref()
.ok_or(ConfigError::NoSources)?
.get::<T>()
.await
} }
pub async fn get_or_prompt<T: Config>() -> Result<T, ConfigError> { pub async fn get_or_prompt<T: Config>() -> Result<T, ConfigError> {
let manager = CONFIG_CLIENT.lock().await; global_client().await?.get_or_prompt::<T>().await
manager
.as_ref()
.ok_or(ConfigError::NoSources)?
.get_or_prompt::<T>()
.await
} }
pub async fn set<T: Config>(config: &T) -> Result<(), ConfigError> { pub async fn set<T: Config>(config: &T) -> Result<(), ConfigError> {
let manager = CONFIG_CLIENT.lock().await; global_client().await?.set::<T>(config).await
manager
.as_ref()
.ok_or(ConfigError::NoSources)?
.set::<T>(config)
.await
} }
pub fn default_config_dir() -> Option<PathBuf> { pub fn default_config_dir() -> Option<PathBuf> {

View File

@@ -1,6 +1,6 @@
use crate::{ConfigClass, ConfigError, ConfigSource}; use crate::{ConfigClass, ConfigError, ConfigSource};
use async_trait::async_trait; use async_trait::async_trait;
use log::{debug, info}; use log::debug;
pub struct EnvSource; pub struct EnvSource;

View File

@@ -3,46 +3,22 @@ mod deployment_grants;
mod openbao_policy; mod openbao_policy;
pub mod store; pub mod store;
use crate::config::SECRET_NAMESPACE;
use async_trait::async_trait; 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 interactive_parse::InteractiveParseObj;
use log::debug;
use log::info;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Serialize, de::DeserializeOwned}; use serde::{Serialize, de::DeserializeOwned};
use std::fmt; use std::fmt;
use store::InfisicalSecretStore;
use store::LocalFileSecretStore;
pub use store::{OpenbaoSecretStore, OpenbaoStoreOptions, ZitadelJwtBearerConfig}; pub use store::{OpenbaoSecretStore, OpenbaoStoreOptions, ZitadelJwtBearerConfig};
use thiserror::Error; use thiserror::Error;
use tokio::sync::OnceCell;
pub use deployment_grants::OpenBaoDeploymentSecretGrants; pub use deployment_grants::OpenBaoDeploymentSecretGrants;
pub use harmony_secret_derive::Secret; pub use harmony_secret_derive::Secret;
pub use openbao_policy::{HARMONY_STATE_SUBPATH, OpenBaoPolicyManager, render_tenant_policy}; 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 { pub trait Secret: Serialize + DeserializeOwned + JsonSchema + InteractiveParseObj + Sized {
const KEY: &'static str; const KEY: &'static str;
} }
// The error enum remains the same.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum SecretStoreError { pub enum SecretStoreError {
#[error("Secret not found for key '{key}' in namespace '{namespace}'")] #[error("Secret not found for key '{key}' in namespace '{namespace}'")]
@@ -61,7 +37,6 @@ pub enum SecretStoreError {
Store(#[from] Box<dyn std::error::Error + Send + Sync>), Store(#[from] Box<dyn std::error::Error + Send + Sync>),
} }
// The trait is now async!
#[async_trait] #[async_trait]
pub trait SecretStore: fmt::Debug + Send + Sync { pub trait SecretStore: fmt::Debug + Send + Sync {
async fn get_raw(&self, namespace: &str, key: &str) -> Result<Vec<u8>, SecretStoreError>; async fn get_raw(&self, namespace: &str, key: &str) -> Result<Vec<u8>, SecretStoreError>;
@@ -72,173 +47,3 @@ pub trait SecretStore: fmt::Debug + Send + Sync {
value: &[u8], value: &[u8],
) -> Result<(), SecretStoreError>; ) -> Result<(), SecretStoreError>;
} }
// Use OnceCell for async-friendly, one-time initialization.
static SECRET_MANAGER: OnceCell<SecretManager> = 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<dyn SecretStore> = 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<dyn SecretStore>,
}
impl SecretManager {
fn new(namespace: String, store: Box<dyn SecretStore>) -> Self {
Self { namespace, store }
}
/// Retrieves and deserializes a secret.
pub async fn get<T: Secret>() -> Result<T, SecretStoreError> {
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<T: Secret>() -> Result<T, SecretStoreError> {
let secret = Self::get::<T>().await;
let manager = get_secret_manager().await;
let prompted = secret.is_err();
let secret = secret.or_else(|e| -> Result<T, SecretStoreError> {
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<T: Secret>(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<String>,
}
#[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 ç <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> <20> 👩‍👩‍👧‍👦 /span> 👩‍👧‍👦 and why not emojis ",
),
],
},
};
SecretManager::set(&secret).await.unwrap();
let value = SecretManager::get::<TestSecret>().await.unwrap();
assert_eq!(value, secret);
}
}