use clap::Parser;
use ipnetwork::IpNetwork;
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use crate::{
deserializers::{
ForgivingVec, option_string_list_deserializer,
string_list_deserializer,
},
entities::{
Timelength,
logger::{LogConfig, LogLevel, StdioLogMode},
},
};
use super::{
DockerRegistry, GitProvider, ProviderAccount, empty_or_redacted,
};
#[derive(Parser)]
#[command(name = "periphery", author, about, version)]
pub struct CliArgs {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(long, short = 'c')]
pub config_path: Option<Vec<PathBuf>>,
#[arg(long, short = 'm')]
pub config_keyword: Option<Vec<String>>,
#[arg(long)]
pub merge_nested_config: Option<bool>,
#[arg(long)]
pub extend_config_arrays: Option<bool>,
#[arg(long)]
pub log_level: Option<tracing::Level>,
}
#[cfg(feature = "cli")]
#[derive(Debug, Clone, clap::Subcommand)]
pub enum Command {
#[clap(alias = "k")]
Key {
#[command(subcommand)]
command: mogh_pki::cli::KeyCommand,
},
}
#[derive(Deserialize)]
pub struct Env {
#[serde(default, alias = "periphery_config_path")]
pub periphery_config_paths: Vec<PathBuf>,
#[serde(
default = "super::default_config_keywords",
alias = "periphery_config_keyword"
)]
pub periphery_config_keywords: Vec<String>,
#[serde(default = "super::default_merge_nested_config")]
pub periphery_merge_nested_config: bool,
#[serde(default = "super::default_extend_config_arrays")]
pub periphery_extend_config_arrays: bool,
pub periphery_private_key: Option<String>,
pub periphery_private_key_file: Option<PathBuf>,
pub periphery_onboarding_key: Option<String>,
pub periphery_onboarding_key_file: Option<PathBuf>,
#[serde(alias = "periphery_core_public_key")]
pub periphery_core_public_keys: Option<Vec<String>>,
pub periphery_passkeys: Option<Vec<String>>,
pub periphery_passkeys_file: Option<PathBuf>,
#[serde(alias = "periphery_core_address")]
pub periphery_core_addresses: Option<Vec<String>>,
pub periphery_core_tls_insecure_skip_verify: Option<bool>,
pub periphery_connect_as: Option<String>,
pub periphery_server_enabled: Option<bool>,
pub periphery_port: Option<u16>,
pub periphery_bind_ip: Option<String>,
pub periphery_root_directory: Option<PathBuf>,
pub periphery_repo_dir: Option<PathBuf>,
pub periphery_stack_dir: Option<PathBuf>,
pub periphery_build_dir: Option<PathBuf>,
pub periphery_default_terminal_command: Option<String>,
pub periphery_disable_terminals: Option<bool>,
#[serde(alias = "periphery_disable_container_exec")]
pub periphery_disable_container_terminals: Option<bool>,
pub periphery_stats_polling_rate: Option<Timelength>,
pub periphery_container_stats_polling_rate: Option<Timelength>,
pub periphery_legacy_compose_cli: Option<bool>,
pub periphery_logging_level: Option<LogLevel>,
pub periphery_logging_stdio: Option<StdioLogMode>,
pub periphery_logging_pretty: Option<bool>,
pub periphery_logging_location: Option<bool>,
pub periphery_logging_ansi: Option<bool>,
pub periphery_logging_timestamps: Option<bool>,
pub periphery_logging_otlp_endpoint: Option<String>,
pub periphery_logging_opentelemetry_service_name: Option<String>,
pub periphery_logging_opentelemetry_scope_name: Option<String>,
pub periphery_pretty_startup_config: Option<bool>,
pub periphery_allowed_ips: Option<ForgivingVec<IpNetwork>>,
pub periphery_include_disk_mounts: Option<ForgivingVec<PathBuf>>,
pub periphery_exclude_disk_mounts: Option<ForgivingVec<PathBuf>>,
pub periphery_ssl_enabled: Option<bool>,
pub periphery_ssl_key_file: Option<String>,
pub periphery_ssl_cert_file: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct PeripheryConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub private_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub onboarding_key: Option<String>,
#[serde(
default,
alias = "core_public_key",
deserialize_with = "option_string_list_deserializer",
skip_serializing_if = "Option::is_none"
)]
pub core_public_keys: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub passkeys: Option<Vec<String>>,
#[serde(
default,
alias = "core_address",
deserialize_with = "string_list_deserializer"
)]
pub core_addresses: Vec<String>,
#[serde(default)]
pub core_tls_insecure_skip_verify: bool,
#[serde(default)]
pub connect_as: String,
pub server_enabled: Option<bool>,
#[serde(default = "default_periphery_port")]
pub port: u16,
#[serde(default = "default_periphery_bind_ip")]
pub bind_ip: String,
#[serde(default)]
pub allowed_ips: ForgivingVec<IpNetwork>,
#[serde(default = "default_ssl_enabled")]
pub ssl_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssl_key_file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssl_cert_file: Option<String>,
#[serde(default = "default_root_directory")]
pub root_directory: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub repo_dir: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stack_dir: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_dir: Option<PathBuf>,
#[serde(default = "default_default_terminal_command")]
pub default_terminal_command: String,
#[serde(default)]
pub disable_terminals: bool,
#[serde(default, alias = "disable_container_exec")]
pub disable_container_terminals: bool,
#[serde(default = "default_stats_polling_rate")]
pub stats_polling_rate: Timelength,
#[serde(default = "default_container_stats_polling_rate")]
pub container_stats_polling_rate: Timelength,
#[serde(default)]
pub legacy_compose_cli: bool,
#[serde(default)]
pub logging: LogConfig,
#[serde(default)]
pub pretty_startup_config: bool,
#[serde(default)]
pub include_disk_mounts: ForgivingVec<PathBuf>,
#[serde(default)]
pub exclude_disk_mounts: ForgivingVec<PathBuf>,
#[serde(default)]
pub secrets: HashMap<String, String>,
#[serde(default, alias = "git_provider")]
pub git_providers: ForgivingVec<GitProvider>,
#[serde(default, alias = "docker_registry")]
pub docker_registries: ForgivingVec<DockerRegistry>,
}
fn default_periphery_port() -> u16 {
8120
}
fn default_periphery_bind_ip() -> String {
"[::]".to_string()
}
fn default_root_directory() -> PathBuf {
"/etc/komodo".parse().unwrap()
}
fn default_default_terminal_command() -> String {
String::from("bash")
}
fn default_stats_polling_rate() -> Timelength {
Timelength::FiveSeconds
}
fn default_container_stats_polling_rate() -> Timelength {
Timelength::ThirtySeconds
}
fn default_ssl_enabled() -> bool {
true
}
impl Default for PeripheryConfig {
fn default() -> Self {
Self {
private_key: None,
onboarding_key: None,
core_public_keys: None,
passkeys: None,
core_addresses: Default::default(),
core_tls_insecure_skip_verify: Default::default(),
connect_as: Default::default(),
server_enabled: Default::default(),
port: default_periphery_port(),
bind_ip: default_periphery_bind_ip(),
root_directory: default_root_directory(),
repo_dir: None,
stack_dir: None,
build_dir: None,
default_terminal_command: default_default_terminal_command(),
disable_terminals: Default::default(),
disable_container_terminals: Default::default(),
stats_polling_rate: default_stats_polling_rate(),
container_stats_polling_rate:
default_container_stats_polling_rate(),
legacy_compose_cli: Default::default(),
logging: Default::default(),
pretty_startup_config: Default::default(),
allowed_ips: Default::default(),
include_disk_mounts: Default::default(),
exclude_disk_mounts: Default::default(),
secrets: Default::default(),
git_providers: Default::default(),
docker_registries: Default::default(),
ssl_enabled: default_ssl_enabled(),
ssl_key_file: None,
ssl_cert_file: None,
}
}
}
impl PeripheryConfig {
pub fn sanitized(&self) -> PeripheryConfig {
PeripheryConfig {
private_key: self.private_key.as_ref().map(|private_key| {
if private_key.starts_with("file:") {
private_key.clone()
} else {
empty_or_redacted(private_key)
}
}),
onboarding_key: self
.onboarding_key
.as_ref()
.map(|key| empty_or_redacted(key)),
core_public_keys: self.core_public_keys.clone(),
passkeys: self.passkeys.as_ref().map(|passkeys| {
passkeys.iter().map(|p| empty_or_redacted(p)).collect()
}),
core_addresses: self.core_addresses.clone(),
core_tls_insecure_skip_verify: self
.core_tls_insecure_skip_verify,
connect_as: self.connect_as.clone(),
server_enabled: self.server_enabled,
port: self.port,
bind_ip: self.bind_ip.clone(),
root_directory: self.root_directory.clone(),
repo_dir: self.repo_dir.clone(),
stack_dir: self.stack_dir.clone(),
build_dir: self.build_dir.clone(),
default_terminal_command: self.default_terminal_command.clone(),
disable_terminals: self.disable_terminals,
disable_container_terminals: self.disable_container_terminals,
stats_polling_rate: self.stats_polling_rate,
container_stats_polling_rate: self.container_stats_polling_rate,
legacy_compose_cli: self.legacy_compose_cli,
logging: self.logging.clone(),
pretty_startup_config: self.pretty_startup_config,
allowed_ips: self.allowed_ips.clone(),
include_disk_mounts: self.include_disk_mounts.clone(),
exclude_disk_mounts: self.exclude_disk_mounts.clone(),
secrets: self
.secrets
.iter()
.map(|(var, secret)| {
(var.to_string(), empty_or_redacted(secret))
})
.collect(),
git_providers: self
.git_providers
.iter()
.map(|provider| GitProvider {
domain: provider.domain.clone(),
https: provider.https,
accounts: provider
.accounts
.iter()
.map(|account| ProviderAccount {
username: account.username.clone(),
token: empty_or_redacted(&account.token),
})
.collect(),
})
.collect(),
docker_registries: self
.docker_registries
.iter()
.map(|provider| DockerRegistry {
domain: provider.domain.clone(),
organizations: provider.organizations.clone(),
accounts: provider
.accounts
.iter()
.map(|account| ProviderAccount {
username: account.username.clone(),
token: empty_or_redacted(&account.token),
})
.collect(),
})
.collect(),
ssl_enabled: self.ssl_enabled,
ssl_key_file: self.ssl_key_file.clone(),
ssl_cert_file: self.ssl_cert_file.clone(),
}
}
pub fn server_enabled(&self) -> bool {
self
.server_enabled
.unwrap_or(self.core_addresses.is_empty())
}
pub fn core_public_keys_spec(&self) -> Option<Vec<String>> {
if let Some(public_keys) = self.core_public_keys.clone() {
return Some(public_keys);
};
if self.server_enabled() {
return None;
}
let path = format!(
"file:{}",
self.root_directory.join("keys/core.pub").display()
);
Some(vec![path])
}
pub fn repo_dir(&self) -> PathBuf {
if let Some(dir) = &self.repo_dir {
dir.to_owned()
} else {
self.root_directory.join("repos")
}
}
pub fn stack_dir(&self) -> PathBuf {
if let Some(dir) = &self.stack_dir {
dir.to_owned()
} else {
self.root_directory.join("stacks")
}
}
pub fn build_dir(&self) -> PathBuf {
if let Some(dir) = &self.build_dir {
dir.to_owned()
} else {
self.root_directory.join("builds")
}
}
pub fn ssl_key_file(&self) -> PathBuf {
if let Some(dir) = &self.ssl_key_file {
dir.into()
} else {
self.root_directory.join("ssl/key.pem")
}
}
pub fn ssl_cert_file(&self) -> PathBuf {
if let Some(dir) = &self.ssl_cert_file {
dir.into()
} else {
self.root_directory.join("ssl/cert.pem")
}
}
}
impl mogh_server::ServerConfig for &PeripheryConfig {
fn bind_ip(&self) -> &str {
&self.bind_ip
}
fn port(&self) -> u16 {
self.port
}
fn ssl_enabled(&self) -> bool {
self.ssl_enabled
}
fn ssl_key_file(&self) -> &str {
static SSL_KEY_FILE: OnceLock<String> = OnceLock::new();
SSL_KEY_FILE.get_or_init(|| {
PeripheryConfig::ssl_key_file(self)
.into_os_string()
.into_string()
.expect("Invalid ssl key file path.")
})
}
fn ssl_cert_file(&self) -> &str {
static SSL_CERT_FILE: OnceLock<String> = OnceLock::new();
SSL_CERT_FILE.get_or_init(|| {
PeripheryConfig::ssl_cert_file(self)
.into_os_string()
.into_string()
.expect("Invalid ssl cert file path.")
})
}
}