chore: clear compile warnings; retire SecretManager #346
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM docker.io/rust:1.94 AS build
|
||||
FROM docker.io/rust:1.97 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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::<BrocadeSwitchAuth>()
|
||||
let config = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
use std::{path::PathBuf, str::FromStr, sync::Arc};
|
||||
|
||||
use harmony::{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::<SudoPassword>()
|
||||
let secret = harmony_config::get_or_prompt::<SudoPassword>()
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("get sudo password: {e}"))?;
|
||||
creds.sudo_password = Some(secret.password);
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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::<OkdAddNodeFirewallConfig>()
|
||||
let config = harmony_config::get_or_prompt::<OkdAddNodeFirewallConfig>()
|
||||
.await
|
||||
.unwrap();
|
||||
let api_creds = harmony::config::secret::OPNSenseApiCredentials {
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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::<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 {
|
||||
kickstart_filename: "inventory.kickstart".to_string(),
|
||||
|
||||
@@ -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::<BrocadeSwitchAuth>()
|
||||
let switch_auth = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
|
||||
.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::<OPNSenseFirewallCredentials>()
|
||||
let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
|
||||
.await
|
||||
.unwrap();
|
||||
let api_creds = SecretManager::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
let api_creds = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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::<SshKeyPair>().await.unwrap();
|
||||
let ssh_key = harmony_config::get_or_prompt::<SshKeyPair>().await.unwrap();
|
||||
|
||||
let ipxe_score = OKDIpxeScore {
|
||||
kickstart_filename,
|
||||
|
||||
@@ -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::<BrocadeSwitchAuth>()
|
||||
let switch_auth = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
|
||||
.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::<OPNSenseFirewallCredentials>()
|
||||
let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
|
||||
.await
|
||||
.unwrap();
|
||||
let api_creds =
|
||||
SecretManager::get_or_prompt::<harmony::config::secret::OPNSenseApiCredentials>()
|
||||
harmony_config::get_or_prompt::<harmony::config::secret::OPNSenseApiCredentials>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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::<OPNSenseFirewallCredentials>()
|
||||
let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
|
||||
.await
|
||||
.expect("Failed to get SSH credentials");
|
||||
let api_creds = SecretManager::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
let api_creds = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
.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,
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use harmony::{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use harmony::{
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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::<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
|
||||
// harmony_inventory_agent's default port. Shared between the install
|
||||
|
||||
@@ -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<harmony::infra::opnsense::OPNSenseFirewall> {
|
||||
name: String::from("fw0"),
|
||||
};
|
||||
|
||||
let ssh_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>()
|
||||
let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
|
||||
.await
|
||||
.unwrap();
|
||||
let api_credentials = SecretManager::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
let api_credentials = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
use harmony::{
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
use harmony::{
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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::<OPNSenseApiCredentials>("fw-primary")
|
||||
let ssh_creds = SecretManager::get_or_prompt::<OPNSenseFirewallCredentials>()
|
||||
let ssh_creds = harmony_config::get_or_prompt::<OPNSenseFirewallCredentials>()
|
||||
.await
|
||||
.expect("Failed to get SSH credentials");
|
||||
|
||||
let api_creds = SecretManager::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
let api_creds = harmony_config::get_or_prompt::<OPNSenseApiCredentials>()
|
||||
.await
|
||||
.expect("Failed to get API credentials");
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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::<NtfyAuth>().await.unwrap();
|
||||
let config = harmony_config::get_or_prompt::<NtfyAuth>().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,
|
||||
|
||||
@@ -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<T: Topology> Interpret<T> for BrocadeEnableSnmpInterpret {
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
let config = SecretManager::get_or_prompt::<BrocadeSwitchAuth>()
|
||||
let config = harmony_config::get_or_prompt::<BrocadeSwitchAuth>()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Vec<NatsCluster>>,
|
||||
namespace: String,
|
||||
) -> 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_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,
|
||||
|
||||
@@ -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<HAClusterTopology> for OKDAddNodeInterpret {
|
||||
.map_err(|e| InterpretError::new(format!("User prompt failed: {e}")))?;
|
||||
|
||||
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 {
|
||||
username: "root".to_string(),
|
||||
private_pem: ssh_key.private,
|
||||
|
||||
@@ -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::<RedhatSecret>().await?;
|
||||
let ssh_key = SecretManager::get_or_prompt::<SshKeyPair>().await?;
|
||||
let redhat_secret = harmony_config::get_or_prompt::<RedhatSecret>()
|
||||
.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 {
|
||||
cluster_name: &topology.get_cluster_name(),
|
||||
|
||||
@@ -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<HAClusterTopology> for OKDOsArtifactsInterpret {
|
||||
topology: &HAClusterTopology,
|
||||
) -> Result<Outcome, InterpretError> {
|
||||
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 {
|
||||
username: fw_creds.username,
|
||||
password: fw_creds.password,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(deprecated)]
|
||||
use harmony::{
|
||||
inventory::Inventory,
|
||||
modules::{
|
||||
|
||||
@@ -501,31 +501,39 @@ pub async fn init_client(client: Arc<ConfigClient>) {
|
||||
*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> {
|
||||
let manager = CONFIG_CLIENT.lock().await;
|
||||
manager
|
||||
.as_ref()
|
||||
.ok_or(ConfigError::NoSources)?
|
||||
.get::<T>()
|
||||
.await
|
||||
global_client().await?.get::<T>().await
|
||||
}
|
||||
|
||||
pub async fn get_or_prompt<T: Config>() -> Result<T, ConfigError> {
|
||||
let manager = CONFIG_CLIENT.lock().await;
|
||||
manager
|
||||
.as_ref()
|
||||
.ok_or(ConfigError::NoSources)?
|
||||
.get_or_prompt::<T>()
|
||||
.await
|
||||
global_client().await?.get_or_prompt::<T>().await
|
||||
}
|
||||
|
||||
pub async fn set<T: Config>(config: &T) -> Result<(), ConfigError> {
|
||||
let manager = CONFIG_CLIENT.lock().await;
|
||||
manager
|
||||
.as_ref()
|
||||
.ok_or(ConfigError::NoSources)?
|
||||
.set::<T>(config)
|
||||
.await
|
||||
global_client().await?.set::<T>(config).await
|
||||
}
|
||||
|
||||
pub fn default_config_dir() -> Option<PathBuf> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{ConfigClass, ConfigError, ConfigSource};
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, info};
|
||||
use log::debug;
|
||||
|
||||
pub struct EnvSource;
|
||||
|
||||
|
||||
@@ -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<dyn std::error::Error + Send + Sync>),
|
||||
}
|
||||
|
||||
// The trait is now async!
|
||||
#[async_trait]
|
||||
pub trait SecretStore: fmt::Debug + Send + Sync {
|
||||
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],
|
||||
) -> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user