use anyhow::Result;
use serde::Deserialize;
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum PitrForgeCommand {
Verify,
Drill,
Restore,
Status,
Teardown,
Test,
Combine,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(rename_all = "kebab-case")]
pub enum PangeaOperation {
Plan,
Apply,
Destroy,
Output,
Synth,
}
#[derive(Debug, Default, Clone)]
pub struct StepResult {
pub outputs: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct FleetConfig {
pub ssh: SshConfig,
pub deploy: DeployConfig,
pub nodes: HashMap<String, NodeOverride>,
pub hooks: HashMap<String, HookPair>,
pub flows: HashMap<String, FlowDef>,
pub secrets: HashMap<String, SecretDef>,
#[serde(skip)]
pub config_dir: std::path::PathBuf,
}
#[derive(Debug, Deserialize)]
pub struct SecretDef {
pub provider: String,
pub item: String,
pub path: String,
#[serde(default = "default_mode")]
pub mode: String,
#[serde(default)]
pub provision_before: Vec<String>,
}
fn default_mode() -> String {
"0600".to_string()
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct SshConfig {
pub connect_timeout: u32,
pub strict_host_key: String,
pub options: HashMap<String, String>,
}
impl Default for SshConfig {
fn default() -> Self {
Self {
connect_timeout: 5,
strict_host_key: "accept-new".to_string(),
options: HashMap::new(),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(default)]
pub struct DeployConfig {
pub show_trace: bool,
pub magic_rollback: bool,
}
impl Default for DeployConfig {
fn default() -> Self {
Self {
show_trace: false,
magic_rollback: true,
}
}
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct NodeOverride {
pub ssh: SshOverride,
pub deploy: DeployOverride,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct SshOverride {
pub connect_timeout: Option<u32>,
pub strict_host_key: Option<String>,
pub options: HashMap<String, String>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct DeployOverride {
pub show_trace: Option<bool>,
pub magic_rollback: Option<bool>,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub struct HookPair {
pub pre: Option<String>,
pub post: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct FlowDef {
#[serde(default)]
pub description: String,
#[serde(default)]
pub secrets: HashMap<String, FlowSecret>,
pub steps: Vec<StepDef>,
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "source", rename_all = "kebab-case")]
pub enum FlowSecret {
Sops {
file: String,
key: String,
},
}
#[derive(Debug, Deserialize)]
pub struct StepDef {
pub id: String,
pub action: ActionDef,
#[serde(default)]
pub targets: Vec<String>,
#[serde(default)]
pub depends_on: Vec<String>,
pub condition: Option<ConditionDef>,
}
#[derive(Debug, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
pub enum ActionDef {
Deploy {
#[serde(default)]
show_trace: bool,
#[serde(default)]
dry_run: bool,
},
Build {
#[serde(default)]
show_trace: bool,
},
Diff,
Status,
Ping,
Rollback,
Reboot,
Exec {
command: Vec<String>,
},
Shell {
command: String,
#[serde(default)]
env: HashMap<String, String>,
},
DarwinRebuild {
#[serde(default)]
show_trace: bool,
},
HomeManagerRebuild {
#[serde(default)]
show_trace: bool,
},
FlakeUpdate {
#[serde(default)]
inputs: Vec<String>,
},
PitrForge {
command: PitrForgeCommand,
#[serde(default)]
tenant: Option<String>,
#[serde(default)]
environment: Option<String>,
#[serde(default)]
restore_time: Option<String>,
#[serde(default)]
app_version: Option<String>,
#[serde(default)]
config: Option<String>,
#[serde(default)]
output_json: Option<String>,
#[serde(default)]
skip_teardown: bool,
#[serde(default)]
env: HashMap<String, String>,
},
Pangea {
file: String,
template: Option<String>,
namespace: String,
operation: PangeaOperation,
#[serde(default)]
env: HashMap<String, String>,
},
}
#[derive(Debug, Deserialize)]
pub struct ConditionDef {
pub command: String,
}
pub struct ResolvedSsh {
pub connect_timeout: u32,
pub strict_host_key: String,
pub options: HashMap<String, String>,
}
#[allow(dead_code)]
pub struct ResolvedDeploy {
pub show_trace: bool,
pub magic_rollback: bool,
}
impl FleetConfig {
pub fn load(dir: &Path) -> Result<Self> {
let path = dir.join("fleet.yaml");
if !path.exists() {
return Ok(Self::default());
}
let contents = std::fs::read_to_string(&path)?;
let mut config: FleetConfig = serde_yaml_ng::from_str(&contents)?;
config.config_dir = dir.to_path_buf();
Ok(config)
}
pub fn resolve_ssh(&self, node_name: &str) -> ResolvedSsh {
let mut resolved = ResolvedSsh {
connect_timeout: self.ssh.connect_timeout,
strict_host_key: self.ssh.strict_host_key.clone(),
options: self.ssh.options.clone(),
};
if let Some(ovr) = self.nodes.get(node_name) {
if let Some(t) = ovr.ssh.connect_timeout {
resolved.connect_timeout = t;
}
if let Some(ref s) = ovr.ssh.strict_host_key {
resolved.strict_host_key = s.clone();
}
for (k, v) in &ovr.ssh.options {
resolved.options.insert(k.clone(), v.clone());
}
}
resolved
}
#[allow(dead_code)]
pub fn resolve_deploy(&self, node_name: &str) -> ResolvedDeploy {
let mut resolved = ResolvedDeploy {
show_trace: self.deploy.show_trace,
magic_rollback: self.deploy.magic_rollback,
};
if let Some(ovr) = self.nodes.get(node_name) {
if let Some(v) = ovr.deploy.show_trace {
resolved.show_trace = v;
}
if let Some(v) = ovr.deploy.magic_rollback {
resolved.magic_rollback = v;
}
}
resolved
}
}