libside 0.3.0

a library for building configuration management tools
Documentation
use super::Context;
pub use crate::generic_apt_package;
use crate::graph::GraphNodeReference;
use crate::requirements::{Requirement, Supports};
use crate::system::{NeverError, System};
use serde::{Deserialize, Serialize};
use std::fmt::Display;

#[macro_export]
macro_rules! generic_apt_package {
    ($vis:vis $struct:ident => $apt_package:literal) => {
        $vis struct $struct($crate::graph::GraphNodeReference);

        impl $crate::builder::apt::AptPackage for $struct {
            const NAME: &'static str = $apt_package;

            fn create(node: $crate::graph::GraphNodeReference) -> Self {
                Self(node)
            }

            fn graph_node(&self) -> GraphNodeReference {
                self.0
            }
        }
    }
}

#[derive(Default)]
pub struct Apt {
    update: Option<GraphNodeReference>,
    global_preconditions: Vec<GraphNodeReference>,
}

impl Apt {
    pub fn global_precondition<R: Requirement>(context: &mut Context<R>, node: GraphNodeReference) {
        let state = context.state::<Apt>();
        state.global_preconditions.push(node);
    }
}

pub trait AptPackage {
    const NAME: &'static str;

    fn create(node: GraphNodeReference) -> Self;

    fn graph_node(&self) -> GraphNodeReference;

    fn install<R: Requirement + Supports<AptInstall> + Supports<AptUpdate>>(
        context: &mut Context<R>,
    ) -> Self
    where
        Self: Sized,
    {
        if context.state::<Apt>().update.is_none() {
            context.state::<Apt>().update = Some(context.add_node(AptUpdate, &[]));
        }
        let state = context.state::<Apt>();
        let updated = state.update;
        let dependencies = updated
            .iter()
            .chain(state.global_preconditions.iter())
            .copied()
            .collect::<Vec<_>>();
        Self::create(context.add_node(AptInstall::new(Self::NAME), dependencies.iter()))
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AptInstall {
    name: String,
}

impl AptInstall {
    pub fn new(name: &str) -> AptInstall {
        AptInstall {
            name: name.to_string(),
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub enum InstallError<S: System> {
    #[error("unable to execute apt-get: {0}")]
    FailedToStart(S::CommandError),

    #[error("apt-get failed: {0} {1}")]
    Unsuccessful(String, String),
}

impl<S: System> From<(&str, &str)> for InstallError<S> {
    fn from(output: (&str, &str)) -> Self {
        InstallError::Unsuccessful(output.0.to_string(), output.1.to_string())
    }
}

#[derive(Debug, thiserror::Error)]
#[error("unable to execute apt-get: {0}")]
pub struct CheckError<S: System>(S::CommandError);

impl Requirement for AptInstall {
    type CreateError<S: System> = InstallError<S>;
    type ModifyError<S: System> = NeverError;
    type DeleteError<S: System> = InstallError<S>;
    type HasBeenCreatedError<S: System> = CheckError<S>;

    fn create<S: crate::system::System>(&self, system: &mut S) -> Result<(), Self::CreateError<S>> {
        let result = system
            .execute_command(
                "apt-get",
                &[
                    "install",
                    "-y",
                    "-q",
                    "--no-install-recommends",
                    self.name.as_str(),
                ],
            )
            .map_err(InstallError::FailedToStart)?;
        result.successful()?;

        Ok(())
    }

    fn modify<S: crate::system::System>(
        &self,
        _system: &mut S,
    ) -> Result<(), Self::ModifyError<S>> {
        Ok(())
    }

    fn delete<S: crate::system::System>(&self, system: &mut S) -> Result<(), Self::DeleteError<S>> {
        let result = system
            .execute_command("apt-get", &["remove", "-y", "-q", &self.name])
            .map_err(InstallError::FailedToStart)?;
        result.successful()?;

        Ok(())
    }

    fn has_been_created<S: crate::system::System>(
        &self,
        system: &mut S,
    ) -> Result<bool, Self::HasBeenCreatedError<S>> {
        let result = system
            .execute_command("dpkg-query", &["-W", "-f=${Status}", &self.name])
            .map_err(CheckError)?;
        if result.is_success() {
            Ok(result.stdout_as_str().starts_with("install"))
        } else {
            Ok(false)
        }
    }

    fn affects(&self, other: &Self) -> bool {
        self.name == other.name
    }

    fn supports_modifications(&self) -> bool {
        false
    }
    fn can_undo(&self) -> bool {
        true
    }
    fn may_pre_exist(&self) -> bool {
        true
    }

    fn verify<S: System>(&self, system: &mut S) -> Result<bool, ()> {
        Ok(self.has_been_created(system).unwrap())
    }

    const NAME: &'static str = "apt_package";
}

impl Display for AptInstall {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "apt({})", self.name)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AptUpdate;

#[derive(Debug, thiserror::Error)]
#[error("unable to execute apt-get: {0}")]
pub struct UpdateError<S: System>(S::CommandError);

impl Requirement for AptUpdate {
    const NAME: &'static str = "apt_update";

    type CreateError<S: System> = UpdateError<S>;
    type ModifyError<S: System> = UpdateError<S>;
    type DeleteError<S: System> = NeverError;
    type HasBeenCreatedError<S: System> = NeverError;

    fn create<S: System>(&self, system: &mut S) -> Result<(), Self::CreateError<S>> {
        system
            .execute_command("apt-get", &["update"])
            .map_err(UpdateError)
            .map(|_| ())
    }

    fn modify<S: System>(&self, system: &mut S) -> Result<(), Self::ModifyError<S>> {
        self.create(system)
    }

    fn delete<S: System>(&self, _system: &mut S) -> Result<(), Self::DeleteError<S>> {
        Ok(())
    }

    fn has_been_created<S: System>(
        &self,
        _system: &mut S,
    ) -> Result<bool, Self::HasBeenCreatedError<S>> {
        Ok(true)
    }

    fn affects(&self, _other: &Self) -> bool {
        true
    }

    fn supports_modifications(&self) -> bool {
        true
    }

    fn can_undo(&self) -> bool {
        false
    }

    fn may_pre_exist(&self) -> bool {
        true
    }

    fn verify<S: System>(&self, _system: &mut S) -> Result<bool, ()> {
        Ok(true)
    }
}

impl Display for AptUpdate {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "apt-update")
    }
}

#[cfg(test)]
mod tests {
    use crate::{
        builder::apt::{AptInstall, AptUpdate},
        requirements::Requirement,
        testing::LxcInstance,
    };

    #[test]
    pub fn serialize_deserialize_apt_install() {
        let r = AptInstall {
            name: "test".to_string(),
        };
        let json = r#"{"name":"test"}"#;

        assert_eq!(serde_json::to_string(&r).unwrap(), json);
        assert_eq!(r, serde_json::from_str(json).unwrap());
    }

    #[test]
    pub fn serialize_deserialize_apt_update() {
        let r = AptUpdate;
        let json = r#"null"#;

        assert_eq!(serde_json::to_string(&r).unwrap(), json);
        assert_eq!(r, serde_json::from_str(json).unwrap());
    }

    #[test]
    #[ignore]
    pub fn lxc_apt_install() {
        let mut sys = LxcInstance::start(LxcInstance::DEFAULT_IMAGE);
        let p = AptInstall {
            name: "nginx".to_string(),
        };

        assert!(!p.has_been_created(&mut sys).unwrap());
        assert!(!p.verify(&mut sys).unwrap());

        p.create(&mut sys).unwrap();

        assert!(p.has_been_created(&mut sys).unwrap());
        assert!(p.verify(&mut sys).unwrap());

        p.delete(&mut sys).unwrap();

        assert!(!p.has_been_created(&mut sys).unwrap());
        assert!(!p.verify(&mut sys).unwrap());
    }

    #[test]
    #[ignore]
    pub fn lxc_apt_update() {
        let mut sys = LxcInstance::start(LxcInstance::DEFAULT_IMAGE);
        let p = AptUpdate;

        p.create(&mut sys).unwrap();
        p.modify(&mut sys).unwrap();
        p.delete(&mut sys).unwrap();
    }
}