From 74bfea3434a24be56f0f87bf1bd3685beda0704f Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Wed, 19 Apr 2023 21:49:30 +0200 Subject: [PATCH] Why does it not compile? --- .gitignore | 1 + Cargo.lock | 366 +++++++++++++++++++++++++++++++ Cargo.toml | 26 +++ src/facts/mod.rs | 19 ++ src/facts/os.rs | 155 +++++++++++++ src/lib.rs | 7 + src/main.rs | 28 +++ src/modules/command/builder.rs | 142 ++++++++++++ src/modules/command/condition.rs | 49 +++++ src/modules/command/mod.rs | 117 ++++++++++ src/modules/mod.rs | 132 +++++++++++ src/modules/package/apt.rs | 34 +++ src/modules/package/builder.rs | 103 +++++++++ src/modules/package/list.rs | 110 ++++++++++ src/modules/package/mod.rs | 131 +++++++++++ src/modules/package/pacman.rs | 36 +++ src/utils/cmd.rs | 119 ++++++++++ src/utils/mod.rs | 1 + 18 files changed, 1576 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/facts/mod.rs create mode 100644 src/facts/os.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/modules/command/builder.rs create mode 100644 src/modules/command/condition.rs create mode 100644 src/modules/command/mod.rs create mode 100644 src/modules/mod.rs create mode 100644 src/modules/package/apt.rs create mode 100644 src/modules/package/builder.rs create mode 100644 src/modules/package/list.rs create mode 100644 src/modules/package/mod.rs create mode 100644 src/modules/package/pacman.rs create mode 100644 src/utils/cmd.rs create mode 100644 src/utils/mod.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c9b4428 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,366 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd4056f63fce3b82d852c3da92b08ea59959890813a7f4ce9c0ff85b10cf301b" +dependencies = [ + "quote", + "syn 2.0.14", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "duct" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ae3fc31835f74c2a7ceda3aeede378b0ae2e74c8f1c36559fcc9ae2a4e7d3e" +dependencies = [ + "libc", + "once_cell", + "os_pipe", + "shared_child", +] + +[[package]] +name = "erased-serde" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2b0c2380453a92ea8b6c8e5f64ecaafccddde8ceab55ff7a8ac1029f894569" +dependencies = [ + "serde", +] + +[[package]] +name = "ghost" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e77ac7b51b8e6313251737fcef4b1c01a2ea102bde68415b62c0ee9268fec357" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.14", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "inventory" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7741301a6d6a9b28ce77c0fb77a4eb116b6bc8f3bef09923f7743d059c4157d3" +dependencies = [ + "ctor", + "ghost", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "libc" +version = "0.2.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "os_pipe" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53dbb20faf34b16087a931834cba2d7a73cc74af2b7ef345a4c8324e2409a12" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "rustible" +version = "0.1.0" +dependencies = [ + "duct", + "regex", + "serde", + "serde_json", + "snafu", + "typetag", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.14", +] + +[[package]] +name = "serde_json" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shared_child" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d94659ad3c2137fef23ae75b03d5241d633f8acded53d672decfa0e6e0caef" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "snafu" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0656e7e3ffb70f6c39b3c2a86332bb74aa3c679da781642590f3c1118c5045" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "475b3bbe5245c26f2d8a6f62d67c1f30eb9fffeccee721c45d162c3ebbdf81b2" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "typetag" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc3ebbaab23e6cc369cb48246769d031f5bd85f1b28141f32982e3c0c7b33cf" +dependencies = [ + "erased-serde", + "inventory", + "once_cell", + "serde", + "typetag-impl", +] + +[[package]] +name = "typetag-impl" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb01b60fcc3f5e17babb1a9956263f3ccd2cadc3e52908400231441683283c1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.14", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c5de5d1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "rustible" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1", features = [ "derive" ] } +regex = "1" +duct = "0.13" +#erased-serde = "0.3" +serde_json = "1" +typetag = "0.2" +# Error management +#thiserror = "1" +snafu = "0.7" + +[lib] +name = "rustible" +path = "src/lib.rs" + +[[bin]] +name = "rustible" +path = "src/main.rs" + diff --git a/src/facts/mod.rs b/src/facts/mod.rs new file mode 100644 index 0000000..1cd564b --- /dev/null +++ b/src/facts/mod.rs @@ -0,0 +1,19 @@ +pub mod os; +use os::OsFact; + +#[derive(Clone, Debug, Serialize)] +pub struct Facts { + pub os: OsFact, +} + +impl Facts { + pub fn new() -> Facts { + Facts { + os: OsFact::new(), + } + } + + pub fn os(&self) -> &OsFact { + &self.os + } +} diff --git a/src/facts/os.rs b/src/facts/os.rs new file mode 100644 index 0000000..dc7b0f6 --- /dev/null +++ b/src/facts/os.rs @@ -0,0 +1,155 @@ +use regex::Regex; + +use std::str::FromStr; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum OsFamily { + Debian, + Archlinux, +} + +impl OsFamily { + pub fn as_str(&self) -> &'static str { + match self { + Self::Debian => "debian", + Self::Archlinux => "archlinux", + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct OsFact { + family: OsFamily, + version_int: Option, + version_str: Option, +} + +impl OsFact { + /// Returns an [`OsFact`] from the standard `/etc/os-release` file + pub fn new() -> OsFact { + let content = std::fs::read_to_string("/etc/os-release").expect("Failed to read `/etc/os-release file.`"); + OsFact::from_str(&content) + } + + /// Returns an [`OsFact`] from the passed string slice (used for tests) + pub fn from_str(os_release: &str) -> OsFact { + let re = Regex::new(r#"(?m)^ID=(.*)"#).unwrap(); + if let Some(captures) = re.captures(&os_release) { + if let Some(id) = captures.get(1) { + match id.as_str() { + "debian" => { + let re = Regex::new(r#"(?m)^VERSION="(\d+) \((.*)\)"$"#).unwrap(); + if let Some(captures) = re.captures(&os_release) { + let (v_int, v_str) = (captures.get(1), captures.get(2)); + match (v_int, v_str) { + (Some(v_int), Some(v_str)) => { + let v_int = usize::from_str(v_int.as_str()).expect( + &format!("The OS version from /etc/os-release {} could not be parsed as integer.", v_int.as_str()) + ); + let v_str = v_str.as_str().to_string(); + OsFact { + family: OsFamily::Debian, + version_int: Some(v_int), + version_str: Some(v_str), + } + }, _ => { + panic!("OS was detected as Debian but some versioning info is missing. Integer: {:?}, String: {:?}", v_int, v_str); + } + + } + + } else { + panic!("OS was detected as Debian but version information could not be acquired."); + } + }, "arch" => OsFact { + family: OsFamily::Archlinux, + version_int: None, + version_str: None, + }, _ => { + panic!("OS Family not recognized: {}", id.as_str()); + } + } + } else { + panic!("regex capture `ID` does not exist in /etc/os-release"); + } + } else { + panic!("/etc/os-release does not contain `ID` line"); + } + } + + pub fn family(&self) -> &OsFamily { + &self.family + } +} + +#[cfg(test)] +mod tests { + use super::{OsFact, OsFamily}; + + #[test] + fn can_parse_debian_bookworm() { + let os_release = r#" +PRETTY_NAME="Debian GNU/Linux 12 (bookworm)" +NAME="Debian GNU/Linux" +VERSION_ID="12" +VERSION="12 (bookworm)" +VERSION_CODENAME=bookworm +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +"#; + let fact = OsFact::from_str(os_release); + assert_eq!(fact, OsFact { + family: OsFamily::Debian, + version_int: Some(12), + version_str: Some("bookworm".to_string()) + }); + } + + + #[test] + fn can_parse_debian_bullseye() { + let os_release = r#" +PRETTY_NAME="Debian GNU/Linux 11 (bullseye)" +NAME="Debian GNU/Linux" +VERSION_ID="11" +VERSION="11 (bullseye)" +VERSION_CODENAME=bullseye +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/" +"#; + let fact = OsFact::from_str(os_release); + assert_eq!(fact, OsFact { + family: OsFamily::Debian, + version_int: Some(11), + version_str: Some("bullseye".to_string()) + }); + } + + #[test] + fn can_parse_archlinux() { + let os_release = r#" +NAME="Arch Linux" +PRETTY_NAME="Arch Linux" +ID=arch +BUILD_ID=rolling +ANSI_COLOR="38;2;23;147;209" +HOME_URL="https://archlinux.org/" +DOCUMENTATION_URL="https://wiki.archlinux.org/" +SUPPORT_URL="https://bbs.archlinux.org/" +BUG_REPORT_URL="https://bugs.archlinux.org/" +PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/" +LOGO=archlinux-logo +"#; + let fact = OsFact::from_str(os_release); + assert_eq!(fact, OsFact { + family: OsFamily::Archlinux, + version_int: None, + version_str: None + }); + } + +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..be44f0a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +#[macro_use] extern crate serde; + +pub mod modules; +pub use modules::{Module, ModuleSetup}; +pub mod facts; +pub use facts::Facts; +pub mod utils; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..41ffebf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,28 @@ +use rustible::Facts; +use rustible::modules::{PlaybookRun, Module, Command}; + +fn main() -> Result<(), rustible::modules::command::CommandError> { + let facts = Facts::new(); + println!("Hello, world! Running system {}", facts.os.family().as_str()); + + let mut playbook = PlaybookRun::new(); + + let cmd = Command::new().program("echo").args(&vec!("lol".to_string())).build(); + let res = cmd.run()?; + println!("{}", res.success()); + + let cmd = Command::new().program("echo").args(&vec!("lol".to_string())).build(); + playbook.run(cmd)?; + + let cmd = Command::new() + .creates("/tmp/lol") + .program("touch") + .arg("/tmp/lol") + .build(); + playbook.run(cmd)?; + //let res = cmd.run + + playbook.print_json_pretty(); + + Ok(()) +} \ No newline at end of file diff --git a/src/modules/command/builder.rs b/src/modules/command/builder.rs new file mode 100644 index 0000000..c5f6692 --- /dev/null +++ b/src/modules/command/builder.rs @@ -0,0 +1,142 @@ +use std::path::{Path, PathBuf}; + +use crate::modules::Module; +use super::{CommandModule, CommandError, CommandStatus}; +use super::condition::{SomeCondition}; + +use crate::utils::cmd::Cmd; + +pub struct NoCmd; + +pub struct NoCondition; +#[derive(Clone, Debug, Serialize)] +pub struct CreatesCondition(pub PathBuf); +#[derive(Clone, Debug, Serialize)] +pub struct RemovesCondition(pub PathBuf); +#[derive(Clone, Debug, Serialize)] +pub struct BothConditions(pub CreatesCondition, pub RemovesCondition); + +/// Build a [`CommandModule`]. +/// You need to define the program/args before you can set the directory, +/// or populate stdin. +pub struct CommandBuilder { + cmd: Program, + condition: Option, +} + +impl CommandBuilder { + pub fn new() -> Self { + CommandBuilder { + cmd: NoCmd, + condition: None, + } + } +} + +impl CommandBuilder { + pub fn program>(self, program: T) -> CommandBuilder { + let Self { condition, .. } = self; + + CommandBuilder { + cmd: Cmd::new(program), + condition, + } + } +} + +impl CommandBuilder { + pub fn arg>(self, arg: T) -> CommandBuilder { + let Self { cmd, condition, .. } = self; + + Self { + cmd: cmd.arg(arg), + condition, + } + } + + pub fn args>(self, args: & [ T ]) -> CommandBuilder { + let Self { cmd, condition, .. } = self; + + Self { + cmd: cmd.args(args), + condition, + } + } + + pub fn stdin>(self, input: T) -> CommandBuilder { + let Self { cmd, condition, .. } = self; + + CommandBuilder { + cmd: cmd.stdin(input), + condition, + } + } + + pub fn chdir>(self, dir: T) -> CommandBuilder { + let Self { cmd, condition, .. } = self; + + CommandBuilder { + cmd: cmd.chdir(dir), + condition, + } + } +} + +impl CommandBuilder { + pub fn creates>(self, path: T) -> CommandBuilder { + let Self { cmd, .. } = self; + + CommandBuilder { + cmd, + condition: Some(CreatesCondition(path.as_ref().to_path_buf())), + } + } + + pub fn removes>(self, path: T) -> CommandBuilder { + let Self { cmd, .. } = self; + + CommandBuilder { + cmd, + condition: Some(RemovesCondition(path.as_ref().to_path_buf())), + } + } +} + +impl CommandBuilder { + pub fn removes>(self, path: T) -> CommandBuilder { + let Self {cmd, condition, .. } = self; + + CommandBuilder { + cmd, + condition: Some(BothConditions(condition.unwrap(), RemovesCondition(path.as_ref().to_path_buf()))), + } + } +} + +impl CommandBuilder { + pub fn creates>(self, path: T) -> CommandBuilder { + let Self { cmd, condition, .. } = self; + + CommandBuilder { + cmd, + condition: Some(BothConditions(CreatesCondition(path.as_ref().to_path_buf()), condition.unwrap())), + } + } +} + +impl>> CommandBuilder { + pub fn build(self) -> CommandModule { + let Self { cmd, condition, .. } = self; + + let condition: Option = condition.map(|x| x.into()).flatten(); + + CommandModule { + cmd, + condition, + } + } + + pub fn run(self) -> Result { + self.build().run().into() + } +} diff --git a/src/modules/command/condition.rs b/src/modules/command/condition.rs new file mode 100644 index 0000000..b4a2caf --- /dev/null +++ b/src/modules/command/condition.rs @@ -0,0 +1,49 @@ +use super::builder::{NoCondition, CreatesCondition, RemovesCondition, BothConditions}; + +#[derive(Clone, Debug, Serialize)] +pub enum SomeCondition { + #[serde(rename="creates")] + Creates(CreatesCondition), + #[serde(rename="removes")] + Removes(RemovesCondition), + #[serde(rename="creates_and_removes")] + Both(BothConditions), +} + +impl SomeCondition { + pub fn should_run(&self) -> bool { + match self { + Self::Creates(c) => { + ! c.0.exists() + }, Self::Removes(c) => { + c.0.exists() + }, Self::Both(c) => { + ! c.0.0.exists() || c.1.0.exists() + } + } + } +} + +impl From for Option { + fn from(c: CreatesCondition) -> Option { + Some(SomeCondition::Creates(c)) + } +} + +impl From for Option { + fn from(c: RemovesCondition) -> Option { + Some(SomeCondition::Removes(c)) + } +} + +impl From for Option { + fn from(c: BothConditions) -> Option { + Some(SomeCondition::Both(c)) + } +} + +impl From for Option { + fn from(_c: NoCondition) -> Option { + None + } +} diff --git a/src/modules/command/mod.rs b/src/modules/command/mod.rs new file mode 100644 index 0000000..56972e7 --- /dev/null +++ b/src/modules/command/mod.rs @@ -0,0 +1,117 @@ +use crate::utils::cmd::Cmd; + +use std::path::PathBuf; + +pub mod builder; +pub mod condition; + +use builder::{CommandBuilder, NoCondition, NoCmd}; +use condition::SomeCondition; + +use crate::{Module, ModuleSetup, Facts}; + +/// A copy of the argument passed to the [`CommandBuilder`], +/// as returned serialized in the task run output +#[derive(Clone, Debug, Serialize)] +pub struct CommandArgs { + #[serde(flatten)] + pub args: Cmd, + pub condition: Option, +} + +pub struct CommandModule { + cmd: Cmd, + condition: Option, +} + +impl CommandModule { + pub fn new() -> CommandBuilder { + CommandBuilder::new() + } +} + +impl ModuleSetup for CommandModule { + fn with_facts(self, _facts: &Facts) -> Box> { + Box::new(self) + } +} + +impl Module for CommandModule { + + fn serialize_args(&self) -> serde_json::Value { + serde_json::to_value(&CommandArgs { + args: self.cmd.clone(), + condition: self.condition.clone(), + }).unwrap() + } + + fn module_name(&self) -> &'static str { + "command" + } + + fn run(self) -> Result { + // The command was built successfully, run it + let Self { cmd, condition, .. } = self; + + // Check conditions + if let Some(condition) = condition { + if ! condition.should_run() { + return Ok(CommandStatus::Skipped(condition)); + } + } + + let res = cmd.run()?; + + Ok(CommandStatus::Done( + CommandReturn { + success: res.success, + stdin: res.stdin, + dir: res.dir, + stdout: res.stdout, + stderr: res.stderr, + } + )) + } +} + +/// Command module has inner parameters to determine whether the command should run (`creates` and `removes`). Because of this, +/// it returns a special [`CommandStatus`] that can be Done or Skipped. These variants are flattened into the textual/JSON output +#[derive(Clone, Debug, Serialize)] +#[serde(tag="status")] +pub enum CommandStatus { + #[serde(rename="done")] + Done(CommandReturn), + #[serde(rename="skip")] + Skipped(SomeCondition), +} + +impl CommandStatus { + pub fn success(&self) -> bool { + match self { + Self::Done(ret) => ret.success, + Self::Skipped(_c) => true, + } + } +} + +/// Return of a Command that was effectively run +#[derive(Clone, Debug, Serialize)] +pub struct CommandReturn { + pub success: bool, + pub stdin: Option, + pub dir: Option, + pub stdout: String, + pub stderr: String, +} + +#[derive(Debug, Serialize)] +pub struct CommandSkipped(SomeCondition); + +#[derive(Clone, Debug, Serialize)] +pub struct CommandError(String); + +impl From for CommandError { + fn from(e: std::io::Error) -> CommandError { + CommandError(e.to_string()) + } +} \ No newline at end of file diff --git a/src/modules/mod.rs b/src/modules/mod.rs new file mode 100644 index 0000000..d11880a --- /dev/null +++ b/src/modules/mod.rs @@ -0,0 +1,132 @@ +//use erased_serde::Serialize; +use serde::Serialize; + +use crate::Facts; + +pub mod package; +pub mod command; + +#[derive(Clone, Debug, Serialize)] +pub struct TaskRun { + /// the arguments passed to the module + module_args: serde_json::Value, + //name: String, + /// the name of the module + module: String, + #[serde(flatten)] + /// the status of the task run (ok, skip, fail) + status: TaskStatus, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(tag="status", content = "return")] +pub enum TaskStatus { + #[serde(rename="ok")] + Ok(serde_json::Value), + #[serde(rename="skip")] + Skip(serde_json::Value), + #[serde(rename="fail")] + Fail(serde_json::Value), +} + +impl TaskStatus { + pub fn ok(val: T) -> TaskStatus { + TaskStatus::Ok(serde_json::to_value(val).unwrap()) + } + + pub fn skip(val: T) -> TaskStatus { + TaskStatus::Skip(serde_json::to_value(val).unwrap()) + } + + pub fn fail(val: T) -> TaskStatus { + TaskStatus::Fail(serde_json::to_value(val).unwrap()) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct PlaybookRun { + pub facts: Facts, + pub steps: Vec, +} + +impl PlaybookRun { + pub fn new() -> PlaybookRun { + PlaybookRun { + facts: Facts::new(), + steps: Vec::new(), + } + } + + pub fn add_step(&mut self, step: TaskRun) { + self.steps.push(step); + } + + pub fn print_json(&self) { + println!("{}", serde_json::to_string(&self).unwrap()); + } + + pub fn print_json_pretty(&self) { + println!("{}", serde_json::to_string_pretty(&self).unwrap()); + } + + pub fn run, A: Serialize, S: Serialize, E: Serialize>(&mut self, cmd: MS) -> Result { + let module = cmd.with_facts(&self.facts); + self.run_inner(module) + } + + pub fn run_inner(&mut self, cmd: Box>) -> Result { + // TODO: Check conditions for skip + + let module_name = cmd.module_name().to_string(); + let args = cmd.serialize_args(); + + let res = cmd.run(); + + let status = match &res { + Ok(s) => { + //self.add_step(s) + TaskStatus::ok(s) + }, Err(e) => { + TaskStatus::fail(e) + } + }; + + let run = TaskRun { + module_args: args, + module: module_name, + status, + }; + + self.add_step(run); + + res + } +} + +// pub trait Loopable { +// fn loop_with(self, looped: dyn Fn(Self) -> Self, with: Vec); +// } + +/// A trait for modules to be run in the context of a playbook, by gathering facts +pub trait ModuleSetup { + // type module: Module; + + fn with_facts(self, facts: &Facts) -> Box>; +} + +/// A Module takes some arguments which can be serialized back to the playbook run, and can be run to produce +/// a certain result. +pub trait Module: Sized { + /// Return the module name as string slice + fn module_name(&self) -> &'static str; + + /// Clone the arguments to be stored in the playbook's task run + fn serialize_args(&self) -> serde_json::Value; + + /// Run the module, producing a result + fn run(self) -> Result; +} + +/// Declare module types +pub use command::CommandModule as Command; + diff --git a/src/modules/package/apt.rs b/src/modules/package/apt.rs new file mode 100644 index 0000000..0fed83f --- /dev/null +++ b/src/modules/package/apt.rs @@ -0,0 +1,34 @@ +use crate::utils::cmd::Cmd; + +use super::{PackageError, PackageList, SpecificPackageManager}; + +#[derive(Debug)] +pub struct DebianPackageManager; + +impl SpecificPackageManager for DebianPackageManager { + fn name(&self) -> &'static str { + "apt" + } + + fn update(&self) -> Result<(), PackageError> { + let res = Cmd::new("apt").arg("update").run()?; + + if ! res.success { + return Err(PackageError::CmdFail(res)); + } + Ok(()) + } + + fn install(&self, list: PackageList) -> Result<(), PackageError> { + let res = Cmd::new("apt").arg("install").args(list.list()).run()?; + + if ! res.success { + return Err(PackageError::CmdFail(res)); + } + Ok(()) + } + + fn remove(&self, _list: PackageList) -> Result<(), PackageError> { + todo!() + } +} diff --git a/src/modules/package/builder.rs b/src/modules/package/builder.rs new file mode 100644 index 0000000..8a33a32 --- /dev/null +++ b/src/modules/package/builder.rs @@ -0,0 +1,103 @@ +use super::{PackageArgs, IntoPackageList, PackageList, PackageState, PackageModule, PackageStatus, PackageError, SpecificPackageManager}; +use super::apt::DebianPackageManager; +use super::pacman::ArchlinuxPackageManager; +use std::boxed::Box; + +use crate::Module; + +pub struct PackageArgsBuilder { + name: Packages, + state: State, + manager: Manager, +} + +pub struct NoPackage; +pub struct NoState; +pub struct NoManager; + +impl PackageArgsBuilder { + pub fn new() -> PackageArgsBuilder { + PackageArgsBuilder { + name: NoPackage, + state: NoState, + manager: NoManager, + } + } +} + +impl PackageArgsBuilder { + pub fn name(self, list: T) -> PackageArgsBuilder { + let Self { state, manager, .. } = self; + + PackageArgsBuilder { + name: list.into_package_list(), + state, + manager, + } + } +} + +impl PackageArgsBuilder { + pub fn state(self, state: PackageState) -> PackageArgsBuilder { + let Self { name, manager, .. } = self; + + PackageArgsBuilder { + name, + state, + manager, + } + } +} + +impl PackageArgsBuilder { + pub fn with(self, manager: impl SpecificPackageManager + 'static) -> PackageArgsBuilder> { + let Self { name, state, .. } = self; + + PackageArgsBuilder { + name, + state, + manager: Box::new(manager), + } + } + + pub fn with_apt(self) -> PackageArgsBuilder> { + let Self { name, state, .. } = self; + PackageArgsBuilder { + name, + state, + manager: Box::new(DebianPackageManager), + } + } + + pub fn with_pacman(self) -> PackageArgsBuilder> { + let Self { name, state, .. } = self; + PackageArgsBuilder { + name, + state, + manager: Box::new(ArchlinuxPackageManager), + } + } +} + + +// We can only RUN the args when a package manager has been set. Alternatively, +// when called inside a playbook, the package manager can be populated automatically from facts. +impl PackageArgsBuilder> { + pub fn run(self) -> Result { + self.build().run() + } +} + +impl PackageArgsBuilder> { + pub fn build(self) -> PackageModule> { + let Self { name, state, manager, .. } = self; + + PackageModule { + args: PackageArgs { + name, + state, + }, + manager, + } + } +} diff --git a/src/modules/package/list.rs b/src/modules/package/list.rs new file mode 100644 index 0000000..beeef0c --- /dev/null +++ b/src/modules/package/list.rs @@ -0,0 +1,110 @@ +use serde::Serialize; + +use crate::facts::{Facts, os::OsFamily}; +use crate::utils::cmd::CmdOutput; + +use std::boxed::Box; + +pub mod pacman; +use pacman::ArchlinuxPackageManager; +pub mod apt; +use apt::DebianPackageManager; + +pub mod builder; +use builder::{NoPackage, PackageArgsBuilder}; + +#[derive(Clone, Debug, Serialize)] +pub struct PackageArgs { + name: PackageList, + state: PackageState, +} + +#[derive(Clone, Debug, Serialize)] +pub enum PackageState { + Present, + Absent, + Latest, +} + +#[derive(Debug)] +pub struct PackageModule { + manager: Manager, + args: PackageArgs, +} + +impl Module for PackageModule { + fn clone_args(&self) -> PackageArgs { + self.args.clone() + } + + fn module_name() -> &'static str { + "package" + } +} + +impl Module for PackageModule> { + fn run(&self) -> Result<(), PackageError> { + Ok(()) + } +} + + +impl Module for PackageModule { + fn with_facts(self, facts: &Facts) -> PackageModule> { + let Self { args, .. } = self; + + let manager = match facts.os.family() { + OsFamily::Debian => Box::new(DebianPackageManager), + OsFamily::Archlinux => Box::new(ArchlinuxPackageManager), + }; + + PackageModule { + args, + manager, + } + } +} + + +impl PackageModule { + pub fn new() -> PackageArgsBuilder { + PackageArgsBuilder::new() + } + + pub fn from_args_with_facts(args: PackageArgs, facts: &Facts) -> PackageModule { + let manager = match facts.os.family() { + OsFamily::Debian => PackageModule(Box::new(DebianPackageManager)), + OsFamily::Archlinux => PackageModule(Box::new(ArchlinuxPackageManager)), + }; + PackageModule { + manager, + args, + } + } + + pub fn from_args_with(args: PackageArgs, manager: SpecificPackageManager) -> PackageManager { + PackageModule { + manager: Box::new(manager), + args, + } + } +} + +#[derive(Clone, Debug, Serialize)] +pub enum PackageError { + IoError(String), + CmdFail(CmdOutput), +} + +impl From for PackageError { + fn from(e: std::io::Error) -> PackageError { + PackageError::IoError(e.to_string()) + } +} + +pub trait SpecificPackageManager: std::fmt::Debug { + fn name(&self) -> &'static str; + fn update(&self) -> Result<(), PackageError>; + fn install(&self, list: PackageList) -> Result<(), PackageError>; + fn remove(&self, list: PackageList) -> Result<(), PackageError>; +} diff --git a/src/modules/package/mod.rs b/src/modules/package/mod.rs new file mode 100644 index 0000000..a6299e4 --- /dev/null +++ b/src/modules/package/mod.rs @@ -0,0 +1,131 @@ +use serde::Serialize; + +use crate::{Module, ModuleSetup}; +use crate::facts::{Facts, os::OsFamily}; +use crate::utils::cmd::CmdOutput; + +use std::boxed::Box; + +pub mod pacman; +use pacman::ArchlinuxPackageManager; +pub mod apt; +use apt::DebianPackageManager; + +pub mod builder; +pub use builder::PackageArgsBuilder; + +pub type PackageStatus = (); + +#[derive(Clone, Debug, Serialize)] +pub struct PackageArgs { + name: PackageList, + state: PackageState, +} + +#[derive(Clone, Debug, Serialize)] +pub enum PackageState { + Present, + Absent, + Latest, +} + +#[derive(Debug)] +pub struct PackageModule { + manager: Manager, + args: PackageArgs, +} + + +#[derive(Debug)] +pub struct NoManager; + +impl Module for PackageModule> { + fn serialize_args(&self) -> serde_json::Value { + serde_json::to_value(&self.args).unwrap() + } + + fn module_name(&self) -> &'static str { + "package" + } + + fn run(self) -> Result<(), PackageError> { + Ok(()) + } +} + +impl ModuleSetup for PackageModule { + fn with_facts(self, facts: &Facts) -> Box> { + let Self { args, .. } = self; + + let manager: Box = match facts.os.family() { + OsFamily::Debian => Box::new(DebianPackageManager), + OsFamily::Archlinux => Box::new(ArchlinuxPackageManager), + }; + + Box::new(PackageModule { + args, + manager, + }) + } +} + + +impl PackageModule { + pub fn new() -> PackageArgsBuilder { + PackageArgsBuilder::new() + } +} + +#[derive(Clone, Debug, Serialize)] +pub enum PackageError { + IoError(String), + CmdFail(CmdOutput), +} + +impl From for PackageError { + fn from(e: std::io::Error) -> PackageError { + PackageError::IoError(e.to_string()) + } +} + +pub trait SpecificPackageManager: std::fmt::Debug { + fn name(&self) -> &'static str; + fn update(&self) -> Result<(), PackageError>; + fn install(&self, list: PackageList) -> Result<(), PackageError>; + fn remove(&self, list: PackageList) -> Result<(), PackageError>; +} + +#[derive(Clone, Debug, Serialize)] +pub struct PackageList { + pub list: Vec, +} + +impl PackageList { + pub fn list(&self) -> & [ String ] { + &self.list + } + + pub fn add(self, add: T) -> PackageList { + let Self { mut list } = self; + + let extend_with = add.into_package_list(); + list.extend(extend_with.list); + + PackageList { + list + } + } +} + +pub trait IntoPackageList { + fn into_package_list(self) -> PackageList; +} + +impl >> IntoPackageList for T { + fn into_package_list(self) -> PackageList { + PackageList { + list: self.into() + } + } +} + diff --git a/src/modules/package/pacman.rs b/src/modules/package/pacman.rs new file mode 100644 index 0000000..20c5986 --- /dev/null +++ b/src/modules/package/pacman.rs @@ -0,0 +1,36 @@ +use crate::utils::cmd::Cmd; + +use super::{PackageError, PackageList, SpecificPackageManager}; + +#[derive(Debug)] +pub struct ArchlinuxPackageManager; + +impl SpecificPackageManager for ArchlinuxPackageManager { + fn name(&self) -> &'static str { + "pacman" + } + + fn update(&self) -> Result<(), PackageError> { + let res = Cmd::new("pacman").arg("-Sy").run()?; + + if ! res.success { + return Err(PackageError::CmdFail(res)); + } + + Ok(()) + } + + fn install(&self, list: PackageList) -> Result<(), PackageError> { + let res = Cmd::new("pacman").arg("-S").args(list.list()).run()?; + + if ! res.success { + return Err(PackageError::CmdFail(res)); + } + + Ok(()) + } + + fn remove(&self, _list: PackageList) -> Result<(), PackageError> { + todo!() + } +} diff --git a/src/utils/cmd.rs b/src/utils/cmd.rs new file mode 100644 index 0000000..08da934 --- /dev/null +++ b/src/utils/cmd.rs @@ -0,0 +1,119 @@ +use duct::cmd as duct_cmd; + +use std::path::PathBuf; + +#[derive(Clone, Debug, Serialize)] +pub struct Cmd { + program: String, + args: Vec, + stdin: Option, + chdir: Option, +} + +impl Cmd { + pub fn new>(program: T) -> Cmd { + Cmd { + program: program.as_ref().to_string(), + args: Vec::new(), + stdin: None, + chdir: None, + } + } + + pub fn arg>(self, arg: T) -> Cmd { + let Self { program, mut args, stdin, chdir, .. } = self; + + args.push(arg.as_ref().to_string()); + + Cmd { + program, + args, + stdin, + chdir, + } + } + + pub fn args>(self, new_args: & [ T ]) -> Cmd { + + let Self { program, mut args, stdin, chdir, .. } = self; + + for arg in new_args { + args.push(arg.as_ref().to_string()); + } + + Cmd { + program, + args, + stdin, + chdir, + } } + + pub fn stdin>(self, input: T) -> Cmd { + let Self { program, args, mut stdin, chdir, .. } = self; + + stdin = if let Some(mut s) = stdin { + s.push_str(input.as_ref()); + Some(s) + } else { + Some(input.as_ref().to_string()) + }; + + Cmd { + program, + args, + stdin, + chdir, + } + } + + pub fn chdir>(self, dir: T) -> Cmd { + let Self { program, args, stdin, .. } = self; + + Cmd { + program, + args, + stdin, + chdir: Some(PathBuf::from(dir.as_ref())), + } + } + + pub fn run(self) -> Result { + let Self { program, args, stdin, chdir, .. } = self; + + let mut cmd = duct_cmd(&program, &args); + + if let Some(stdin) = &stdin { + cmd = cmd.stdin_bytes(stdin.as_bytes()); + } + + if let Some(chdir) = &chdir { + cmd = cmd.dir(chdir); + } + + let res = cmd.unchecked() + .stdout_capture() + .stderr_capture() + .run()?; + + Ok(CmdOutput { + program, + args, + success: res.status.success(), + stdin, + dir: chdir, + stdout: String::from_utf8(res.stdout).unwrap(), + stderr: String::from_utf8(res.stderr).unwrap(), + }) + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct CmdOutput { + pub program: String, + pub args: Vec, + pub success: bool, + pub stdin: Option, + pub dir: Option, + pub stdout: String, + pub stderr: String, +} \ No newline at end of file diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..52958ec --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod cmd;