Why does it not compile?

This commit is contained in:
selfhoster selfhoster 2023-04-19 21:49:30 +02:00
commit 74bfea3434
18 changed files with 1576 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

366
Cargo.lock generated Normal file
View File

@ -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"

26
Cargo.toml Normal file
View File

@ -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"

19
src/facts/mod.rs Normal file
View File

@ -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
}
}

155
src/facts/os.rs Normal file
View File

@ -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<usize>,
version_str: Option<String>,
}
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
});
}
}

7
src/lib.rs Normal file
View File

@ -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;

28
src/main.rs Normal file
View File

@ -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(())
}

View File

@ -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<Program, Condition> {
cmd: Program,
condition: Option<Condition>,
}
impl CommandBuilder<NoCmd, NoCondition> {
pub fn new() -> Self {
CommandBuilder {
cmd: NoCmd,
condition: None,
}
}
}
impl<Condition> CommandBuilder<NoCmd, Condition> {
pub fn program<T: AsRef<str>>(self, program: T) -> CommandBuilder<Cmd, Condition> {
let Self { condition, .. } = self;
CommandBuilder {
cmd: Cmd::new(program),
condition,
}
}
}
impl<Condition> CommandBuilder<Cmd, Condition> {
pub fn arg<T: AsRef<str>>(self, arg: T) -> CommandBuilder<Cmd, Condition> {
let Self { cmd, condition, .. } = self;
Self {
cmd: cmd.arg(arg),
condition,
}
}
pub fn args<T: AsRef<str>>(self, args: & [ T ]) -> CommandBuilder<Cmd, Condition> {
let Self { cmd, condition, .. } = self;
Self {
cmd: cmd.args(args),
condition,
}
}
pub fn stdin<T: AsRef<str>>(self, input: T) -> CommandBuilder<Cmd, Condition> {
let Self { cmd, condition, .. } = self;
CommandBuilder {
cmd: cmd.stdin(input),
condition,
}
}
pub fn chdir<T: AsRef<str>>(self, dir: T) -> CommandBuilder<Cmd, Condition> {
let Self { cmd, condition, .. } = self;
CommandBuilder {
cmd: cmd.chdir(dir),
condition,
}
}
}
impl<Args> CommandBuilder<Args, NoCondition> {
pub fn creates<T: AsRef<Path>>(self, path: T) -> CommandBuilder<Args, CreatesCondition> {
let Self { cmd, .. } = self;
CommandBuilder {
cmd,
condition: Some(CreatesCondition(path.as_ref().to_path_buf())),
}
}
pub fn removes<T: AsRef<Path>>(self, path: T) -> CommandBuilder<Args, RemovesCondition> {
let Self { cmd, .. } = self;
CommandBuilder {
cmd,
condition: Some(RemovesCondition(path.as_ref().to_path_buf())),
}
}
}
impl<Args> CommandBuilder<Args, CreatesCondition> {
pub fn removes<T: AsRef<Path>>(self, path: T) -> CommandBuilder<Args, BothConditions> {
let Self {cmd, condition, .. } = self;
CommandBuilder {
cmd,
condition: Some(BothConditions(condition.unwrap(), RemovesCondition(path.as_ref().to_path_buf()))),
}
}
}
impl<Args> CommandBuilder<Args, RemovesCondition> {
pub fn creates<T: AsRef<Path>>(self, path: T) -> CommandBuilder<Args, BothConditions> {
let Self { cmd, condition, .. } = self;
CommandBuilder {
cmd,
condition: Some(BothConditions(CreatesCondition(path.as_ref().to_path_buf()), condition.unwrap())),
}
}
}
impl<Condition: Into<Option<SomeCondition>>> CommandBuilder<Cmd, Condition> {
pub fn build(self) -> CommandModule {
let Self { cmd, condition, .. } = self;
let condition: Option<SomeCondition> = condition.map(|x| x.into()).flatten();
CommandModule {
cmd,
condition,
}
}
pub fn run(self) -> Result<CommandStatus, CommandError> {
self.build().run().into()
}
}

View File

@ -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<CreatesCondition> for Option<SomeCondition> {
fn from(c: CreatesCondition) -> Option<SomeCondition> {
Some(SomeCondition::Creates(c))
}
}
impl From<RemovesCondition> for Option<SomeCondition> {
fn from(c: RemovesCondition) -> Option<SomeCondition> {
Some(SomeCondition::Removes(c))
}
}
impl From<BothConditions> for Option<SomeCondition> {
fn from(c: BothConditions) -> Option<SomeCondition> {
Some(SomeCondition::Both(c))
}
}
impl From<NoCondition> for Option<SomeCondition> {
fn from(_c: NoCondition) -> Option<SomeCondition> {
None
}
}

117
src/modules/command/mod.rs Normal file
View File

@ -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<SomeCondition>,
}
pub struct CommandModule {
cmd: Cmd,
condition: Option<SomeCondition>,
}
impl CommandModule {
pub fn new() -> CommandBuilder<NoCmd, NoCondition> {
CommandBuilder::new()
}
}
impl ModuleSetup<CommandArgs, CommandStatus, CommandError> for CommandModule {
fn with_facts(self, _facts: &Facts) -> Box<dyn Module<CommandArgs, CommandStatus, CommandError>> {
Box::new(self)
}
}
impl Module<CommandArgs, CommandStatus, CommandError> 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<CommandStatus, CommandError> {
// 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<String>,
pub dir: Option<PathBuf>,
pub stdout: String,
pub stderr: String,
}
#[derive(Debug, Serialize)]
pub struct CommandSkipped(SomeCondition);
#[derive(Clone, Debug, Serialize)]
pub struct CommandError(String);
impl From<std::io::Error> for CommandError {
fn from(e: std::io::Error) -> CommandError {
CommandError(e.to_string())
}
}

132
src/modules/mod.rs Normal file
View File

@ -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<T: Serialize>(val: T) -> TaskStatus {
TaskStatus::Ok(serde_json::to_value(val).unwrap())
}
pub fn skip<T: Serialize>(val: T) -> TaskStatus {
TaskStatus::Skip(serde_json::to_value(val).unwrap())
}
pub fn fail<T: Serialize>(val: T) -> TaskStatus {
TaskStatus::Fail(serde_json::to_value(val).unwrap())
}
}
#[derive(Clone, Debug, Serialize)]
pub struct PlaybookRun {
pub facts: Facts,
pub steps: Vec<TaskRun>,
}
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<MS: ModuleSetup<A, S, E>, A: Serialize, S: Serialize, E: Serialize>(&mut self, cmd: MS) -> Result<S, E> {
let module = cmd.with_facts(&self.facts);
self.run_inner(module)
}
pub fn run_inner<A: Serialize, S: Serialize, E: Serialize>(&mut self, cmd: Box<dyn Module<A, S, E>>) -> Result<S, E> {
// 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<T>(self, looped: dyn Fn(Self) -> Self, with: Vec<T>);
// }
/// A trait for modules to be run in the context of a playbook, by gathering facts
pub trait ModuleSetup<A, S, E> {
// type module: Module<A, S, E>;
fn with_facts(self, facts: &Facts) -> Box<dyn Module<A, S, E>>;
}
/// 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<A: Serialize, S: Serialize, E: Serialize>: 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<S, E>;
}
/// Declare module types
pub use command::CommandModule as Command;

View File

@ -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!()
}
}

View File

@ -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<Packages, State, Manager> {
name: Packages,
state: State,
manager: Manager,
}
pub struct NoPackage;
pub struct NoState;
pub struct NoManager;
impl PackageArgsBuilder<NoPackage, NoState, NoManager> {
pub fn new() -> PackageArgsBuilder<NoPackage, NoState, NoManager> {
PackageArgsBuilder {
name: NoPackage,
state: NoState,
manager: NoManager,
}
}
}
impl<State, Manager> PackageArgsBuilder<NoPackage, State, Manager> {
pub fn name<T: IntoPackageList>(self, list: T) -> PackageArgsBuilder<PackageList, State, Manager> {
let Self { state, manager, .. } = self;
PackageArgsBuilder {
name: list.into_package_list(),
state,
manager,
}
}
}
impl<Packages, Manager> PackageArgsBuilder<Packages, NoState, Manager> {
pub fn state(self, state: PackageState) -> PackageArgsBuilder<Packages, PackageState, Manager> {
let Self { name, manager, .. } = self;
PackageArgsBuilder {
name,
state,
manager,
}
}
}
impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> {
pub fn with(self, manager: impl SpecificPackageManager + 'static) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> {
let Self { name, state, .. } = self;
PackageArgsBuilder {
name,
state,
manager: Box::new(manager),
}
}
pub fn with_apt(self) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> {
let Self { name, state, .. } = self;
PackageArgsBuilder {
name,
state,
manager: Box::new(DebianPackageManager),
}
}
pub fn with_pacman(self) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> {
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<PackageList, PackageState, Box<dyn SpecificPackageManager>> {
pub fn run(self) -> Result<PackageStatus, PackageError> {
self.build().run()
}
}
impl PackageArgsBuilder<PackageList, PackageState, Box<dyn SpecificPackageManager>> {
pub fn build(self) -> PackageModule<Box<dyn SpecificPackageManager>> {
let Self { name, state, manager, .. } = self;
PackageModule {
args: PackageArgs {
name,
state,
},
manager,
}
}
}

110
src/modules/package/list.rs Normal file
View File

@ -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: Manager,
args: PackageArgs,
}
impl<Manager> Module<PackageArgs, (), PackageError> for PackageModule<Manager> {
fn clone_args(&self) -> PackageArgs {
self.args.clone()
}
fn module_name() -> &'static str {
"package"
}
}
impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn SpecificPackageManager>> {
fn run(&self) -> Result<(), PackageError> {
Ok(())
}
}
impl Module<PackageArgs, (), PackageError> for PackageModule<NoManager> {
fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn SpecificPackageManager>> {
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<std::io::Error> 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>;
}

131
src/modules/package/mod.rs Normal file
View File

@ -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: Manager,
args: PackageArgs,
}
#[derive(Debug)]
pub struct NoManager;
impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn SpecificPackageManager>> {
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<PackageArgs, (), PackageError> for PackageModule<NoManager> {
fn with_facts(self, facts: &Facts) -> Box<dyn Module<PackageArgs, (), PackageError>> {
let Self { args, .. } = self;
let manager: Box<dyn SpecificPackageManager> = match facts.os.family() {
OsFamily::Debian => Box::new(DebianPackageManager),
OsFamily::Archlinux => Box::new(ArchlinuxPackageManager),
};
Box::new(PackageModule {
args,
manager,
})
}
}
impl PackageModule<NoManager> {
pub fn new() -> PackageArgsBuilder<builder::NoPackage, builder::NoState, builder::NoManager> {
PackageArgsBuilder::new()
}
}
#[derive(Clone, Debug, Serialize)]
pub enum PackageError {
IoError(String),
CmdFail(CmdOutput),
}
impl From<std::io::Error> 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<String>,
}
impl PackageList {
pub fn list(&self) -> & [ String ] {
&self.list
}
pub fn add<T: IntoPackageList>(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 <T: Into<Vec<String>>> IntoPackageList for T {
fn into_package_list(self) -> PackageList {
PackageList {
list: self.into()
}
}
}

View File

@ -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!()
}
}

119
src/utils/cmd.rs Normal file
View File

@ -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<String>,
stdin: Option<String>,
chdir: Option<PathBuf>,
}
impl Cmd {
pub fn new<T: AsRef<str>>(program: T) -> Cmd {
Cmd {
program: program.as_ref().to_string(),
args: Vec::new(),
stdin: None,
chdir: None,
}
}
pub fn arg<T: AsRef<str>>(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<T: AsRef<str>>(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<T: AsRef<str>>(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<T: AsRef<str>>(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<CmdOutput, std::io::Error> {
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<String>,
pub success: bool,
pub stdin: Option<String>,
pub dir: Option<PathBuf>,
pub stdout: String,
pub stderr: String,
}

1
src/utils/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod cmd;