284 lines
8.6 KiB
Rust
284 lines
8.6 KiB
Rust
use photon_config::load_toml_config;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::net::SocketAddr;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
/// Deployer server configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Config {
|
|
/// HTTP server bind address
|
|
#[serde(default = "default_bind_addr")]
|
|
pub bind_addr: SocketAddr,
|
|
|
|
/// ChainFire cluster endpoints
|
|
#[serde(default)]
|
|
pub chainfire: ChainFireConfig,
|
|
|
|
/// PhotonCloud cluster ID (for writing desired state under photoncloud/clusters/...)
|
|
#[serde(default)]
|
|
pub cluster_id: Option<String>,
|
|
|
|
/// Namespace prefix for PhotonCloud cluster state
|
|
#[serde(default = "default_cluster_namespace")]
|
|
pub cluster_namespace: String,
|
|
|
|
/// Node heartbeat timeout (seconds)
|
|
#[serde(default = "default_heartbeat_timeout")]
|
|
pub heartbeat_timeout_secs: u64,
|
|
|
|
/// Local state path for bootstrapper mode (file or directory)
|
|
#[serde(default = "default_local_state_path")]
|
|
pub local_state_path: Option<PathBuf>,
|
|
|
|
/// Optional tar.gz bundle containing the PhotonCloud flake source tree for bootstrap installs
|
|
#[serde(default)]
|
|
pub bootstrap_flake_bundle_path: Option<PathBuf>,
|
|
|
|
/// Shared bootstrap token required for phone-home/admin APIs
|
|
#[serde(default)]
|
|
pub bootstrap_token: Option<String>,
|
|
|
|
/// Shared admin token required for admin APIs
|
|
#[serde(default)]
|
|
pub admin_token: Option<String>,
|
|
|
|
/// Allow admin APIs to fall back to bootstrap token (unsafe; for dev only)
|
|
#[serde(default = "default_allow_admin_fallback")]
|
|
pub allow_admin_fallback: bool,
|
|
|
|
/// Allow unauthenticated requests (unsafe; for dev only)
|
|
#[serde(default = "default_allow_unauthenticated")]
|
|
pub allow_unauthenticated: bool,
|
|
|
|
/// Require ChainFire to be available at startup (fail fast if unavailable)
|
|
#[serde(default = "default_require_chainfire")]
|
|
pub require_chainfire: bool,
|
|
|
|
/// Allow nodes with unknown machine-id to auto-register (unsafe)
|
|
#[serde(default = "default_allow_unknown_nodes")]
|
|
pub allow_unknown_nodes: bool,
|
|
|
|
/// Enable hardcoded test machine mappings (unsafe)
|
|
#[serde(default = "default_allow_test_mappings")]
|
|
pub allow_test_mappings: bool,
|
|
|
|
/// Optional CA certificate path for issuing node TLS certs
|
|
#[serde(default)]
|
|
pub tls_ca_cert_path: Option<String>,
|
|
|
|
/// Optional CA private key path for issuing node TLS certs
|
|
#[serde(default)]
|
|
pub tls_ca_key_path: Option<String>,
|
|
|
|
/// Allow self-signed TLS certificates when no CA is configured
|
|
#[serde(default = "default_tls_self_signed")]
|
|
pub tls_self_signed: bool,
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Self {
|
|
bind_addr: default_bind_addr(),
|
|
chainfire: ChainFireConfig::default(),
|
|
cluster_id: None,
|
|
cluster_namespace: default_cluster_namespace(),
|
|
heartbeat_timeout_secs: default_heartbeat_timeout(),
|
|
local_state_path: default_local_state_path(),
|
|
bootstrap_flake_bundle_path: None,
|
|
bootstrap_token: None,
|
|
admin_token: None,
|
|
allow_admin_fallback: default_allow_admin_fallback(),
|
|
allow_unauthenticated: default_allow_unauthenticated(),
|
|
require_chainfire: default_require_chainfire(),
|
|
allow_unknown_nodes: default_allow_unknown_nodes(),
|
|
allow_test_mappings: default_allow_test_mappings(),
|
|
tls_ca_cert_path: None,
|
|
tls_ca_key_path: None,
|
|
tls_self_signed: default_tls_self_signed(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Config {
|
|
pub fn redacted(&self) -> Self {
|
|
let mut redacted = self.clone();
|
|
if redacted.bootstrap_token.is_some() {
|
|
redacted.bootstrap_token = Some("<redacted>".to_string());
|
|
}
|
|
if redacted.admin_token.is_some() {
|
|
redacted.admin_token = Some("<redacted>".to_string());
|
|
}
|
|
redacted
|
|
}
|
|
|
|
pub fn apply_secret_env_overrides(&mut self) {
|
|
if let Ok(token) = std::env::var("DEPLOYER_BOOTSTRAP_TOKEN") {
|
|
let trimmed = token.trim();
|
|
if !trimmed.is_empty() {
|
|
self.bootstrap_token = Some(trimmed.to_string());
|
|
}
|
|
}
|
|
|
|
if let Ok(token) = std::env::var("DEPLOYER_ADMIN_TOKEN") {
|
|
let trimmed = token.trim();
|
|
if !trimmed.is_empty() {
|
|
self.admin_token = Some(trimmed.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// ChainFire configuration
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ChainFireConfig {
|
|
/// ChainFire cluster endpoints
|
|
#[serde(default = "default_chainfire_endpoints")]
|
|
pub endpoints: Vec<String>,
|
|
|
|
/// Namespace for deployer state
|
|
#[serde(default = "default_chainfire_namespace")]
|
|
pub namespace: String,
|
|
}
|
|
|
|
impl Default for ChainFireConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
endpoints: default_chainfire_endpoints(),
|
|
namespace: default_chainfire_namespace(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn load_config(path: &Path) -> anyhow::Result<Config> {
|
|
let mut config: Config = load_toml_config(path)?;
|
|
config.apply_secret_env_overrides();
|
|
Ok(config)
|
|
}
|
|
|
|
fn default_bind_addr() -> SocketAddr {
|
|
"0.0.0.0:8080".parse().unwrap()
|
|
}
|
|
|
|
fn default_chainfire_endpoints() -> Vec<String> {
|
|
vec![]
|
|
}
|
|
|
|
fn default_chainfire_namespace() -> String {
|
|
"deployer".to_string()
|
|
}
|
|
|
|
fn default_cluster_namespace() -> String {
|
|
"photoncloud".to_string()
|
|
}
|
|
|
|
fn default_heartbeat_timeout() -> u64 {
|
|
300 // 5 minutes
|
|
}
|
|
|
|
fn default_local_state_path() -> Option<PathBuf> {
|
|
Some(PathBuf::from("/var/lib/deployer/state"))
|
|
}
|
|
|
|
fn default_allow_unauthenticated() -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_require_chainfire() -> bool {
|
|
true
|
|
}
|
|
|
|
fn default_allow_admin_fallback() -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_allow_unknown_nodes() -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_allow_test_mappings() -> bool {
|
|
false
|
|
}
|
|
|
|
fn default_tls_self_signed() -> bool {
|
|
false
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::fs;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
fn temp_path(name: &str) -> PathBuf {
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
std::env::temp_dir().join(format!("{}-{}-{}.toml", name, std::process::id(), nanos))
|
|
}
|
|
|
|
#[test]
|
|
fn test_default_config() {
|
|
let config = Config::default();
|
|
assert_eq!(config.bind_addr.to_string(), "0.0.0.0:8080");
|
|
assert_eq!(config.chainfire.namespace, "deployer");
|
|
assert_eq!(config.cluster_namespace, "photoncloud");
|
|
assert!(config.cluster_id.is_none());
|
|
assert_eq!(config.heartbeat_timeout_secs, 300);
|
|
assert_eq!(
|
|
config.local_state_path,
|
|
Some(PathBuf::from("/var/lib/deployer/state"))
|
|
);
|
|
assert!(config.bootstrap_flake_bundle_path.is_none());
|
|
assert!(config.bootstrap_token.is_none());
|
|
assert!(config.admin_token.is_none());
|
|
assert!(!config.allow_admin_fallback);
|
|
assert!(!config.allow_unauthenticated);
|
|
assert!(!config.allow_unknown_nodes);
|
|
assert!(!config.allow_test_mappings);
|
|
assert!(config.tls_ca_cert_path.is_none());
|
|
assert!(config.tls_ca_key_path.is_none());
|
|
assert!(!config.tls_self_signed);
|
|
assert!(config.chainfire.endpoints.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_config_serialization() {
|
|
let config = Config::default();
|
|
let json = serde_json::to_string(&config).unwrap();
|
|
let deserialized: Config = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(deserialized.bind_addr, config.bind_addr);
|
|
}
|
|
|
|
#[test]
|
|
fn test_loads_toml_config() {
|
|
let path = temp_path("deployer-config");
|
|
fs::write(
|
|
&path,
|
|
r#"
|
|
bind_addr = "127.0.0.1:18080"
|
|
cluster_id = "cluster-a"
|
|
allow_unauthenticated = true
|
|
bootstrap_flake_bundle_path = "/tmp/plasmacloud-flake-bundle.tar.gz"
|
|
|
|
[chainfire]
|
|
endpoints = ["http://10.0.0.1:2379"]
|
|
namespace = "bootstrap"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let config = load_config(&path).unwrap();
|
|
assert_eq!(config.bind_addr.to_string(), "127.0.0.1:18080");
|
|
assert_eq!(config.cluster_id.as_deref(), Some("cluster-a"));
|
|
assert_eq!(
|
|
config.bootstrap_flake_bundle_path,
|
|
Some(PathBuf::from("/tmp/plasmacloud-flake-bundle.tar.gz"))
|
|
);
|
|
assert!(config.allow_unauthenticated);
|
|
assert_eq!(config.chainfire.namespace, "bootstrap");
|
|
assert_eq!(config.chainfire.endpoints, vec!["http://10.0.0.1:2379"]);
|
|
|
|
let _ = fs::remove_file(path);
|
|
}
|
|
}
|