aegis-tool 0.3.3

Aegis SSH client and managed host agent.
Documentation
use anyhow::{Result, bail};

use super::{command_output_full_failure_detail, full_error};

pub(super) struct RedeployJob {
    unit: String,
    job: capulus::managed::JobId,
}

impl RedeployJob {
    pub(super) fn new(unit: String, job: String) -> Result<Self> {
        let job = capulus::managed::JobId::parse(&job)?;
        if unit != format!("aegis-redeploy-{job}.service") {
            bail!("Capulus returned invalid redeploy unit \u{60}{unit}\u{60}");
        }
        Ok(Self { unit, job })
    }

    pub(super) fn unit(&self) -> &str {
        &self.unit
    }

    pub(super) fn probe_command(&self) -> String {
        format!(
            "/usr/local/bin/aegis advanced redeploy-status {} --json\n",
            super::sh_quote(&self.job.to_string())
        )
    }

    pub(super) fn probe_local(&self) -> RedeployJobState {
        match crate::managed::status(self.job) {
            Ok(status) => capulus_job_state(status),
            Err(error) => {
                RedeployJobState::Unknown(format!("managed redeploy status probe failed: {error}"))
            }
        }
    }

    pub(super) fn state_from_remote_output(
        &self,
        output: Result<crate::command::CommandOutput>,
    ) -> RedeployJobState {
        match output {
            Ok(output) if output.status.success() => {
                match serde_json::from_str::<capulus::managed::RedeployJob>(output.stdout.trim()) {
                    Ok(status) if status.job == self.job && status.product == "aegis" => {
                        capulus_job_state(status)
                    }
                    Ok(_) => RedeployJobState::Unknown(
                        "managed redeploy status returned the wrong job identity".to_string(),
                    ),
                    Err(error) => RedeployJobState::Unknown(format!(
                        "managed redeploy status returned invalid JSON: {error}"
                    )),
                }
            }
            Ok(output) => RedeployJobState::Unknown(format!(
                "managed redeploy status probe failed: {}",
                command_output_full_failure_detail(&output)
            )),
            Err(error) => RedeployJobState::Unknown(format!(
                "managed redeploy status probe failed: {}",
                full_error(&error)
            )),
        }
    }
}

fn capulus_job_state(status: capulus::managed::RedeployJob) -> RedeployJobState {
    let detail = format!("redeploy: {:?}: {}", status.phase, status.detail);
    match status.phase {
        capulus::managed::JobPhase::Complete => RedeployJobState::Complete(detail),
        capulus::managed::JobPhase::Failed => {
            let rollback = status
                .rollback_succeeded
                .map(|succeeded| {
                    if succeeded {
                        "; rollback succeeded"
                    } else {
                        "; rollback failed"
                    }
                })
                .unwrap_or_default();
            RedeployJobState::Failed(format!("{detail}{rollback}"))
        }
        _ => RedeployJobState::Active(detail),
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(super) enum RedeployJobState {
    Active(String),
    Complete(String),
    Failed(String),
    Unknown(String),
}

impl RedeployJobState {
    pub(super) fn detail(&self) -> &str {
        match self {
            Self::Active(detail)
            | Self::Complete(detail)
            | Self::Failed(detail)
            | Self::Unknown(detail) => detail,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::RedeployJob;

    #[test]
    fn jobs_require_the_exact_capulus_transient_unit() {
        let job = "0123456789abcdef0123456789abcdef";
        let managed =
            RedeployJob::new(format!("aegis-redeploy-{job}.service"), job.to_string()).unwrap();

        assert_eq!(
            format!("/usr/local/bin/aegis advanced redeploy-status {job} --json\n"),
            managed.probe_command()
        );
        assert!(
            RedeployJob::new("aegis-redeploy-other.service".to_string(), job.to_string()).is_err()
        );
        assert!(
            RedeployJob::new(
                format!("aegis-redeploy-{job}.service"),
                "../../etc/passwd".to_string()
            )
            .is_err()
        );
    }
}