diff --git a/Cargo.lock b/Cargo.lock index c9b4428..2c72fff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,73 @@ dependencies = [ "memchr", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "crossbeam-channel" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + [[package]] name = "ctor" version = "0.2.0" @@ -39,6 +106,12 @@ dependencies = [ "shared_child", ] +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "erased-serde" version = "0.3.25" @@ -65,6 +138,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + [[package]] name = "inventory" version = "0.3.5" @@ -93,6 +175,25 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -127,6 +228,28 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "regex" version = "1.7.3" @@ -144,12 +267,25 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "ron" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300a51053b1cb55c80b7a9fde4120726ddf25ca241a1cbb926626f62fb136bff" +dependencies = [ + "base64", + "bitflags", + "serde", +] + [[package]] name = "rustible" version = "0.1.0" dependencies = [ "duct", + "rayon", "regex", + "ron", "serde", "serde_json", "snafu", @@ -162,6 +298,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "serde" version = "1.0.160" diff --git a/Cargo.toml b/Cargo.toml index c5de5d1..0e3e1ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,8 @@ typetag = "0.2" # Error management #thiserror = "1" snafu = "0.7" +ron = "0.8" +rayon = "1.7" [lib] name = "rustible" diff --git a/README.md b/README.md index 9be785d..d910002 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ WIP ansible-like system for sysadmin in rust + +TODO: finish test.sh for testing things diff --git a/examples/facts.rs b/examples/facts.rs new file mode 100644 index 0000000..3e9a790 --- /dev/null +++ b/examples/facts.rs @@ -0,0 +1,6 @@ +use rustible::Facts; + +fn main() { + let facts = Facts::new(); + println!("rustible running system: {}", facts.os.family().as_str()); +} diff --git a/examples/hello.rs b/examples/hello.rs index f9af60c..c03ce01 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,5 +1,3 @@ -use rustible::Facts; - use rustible::modules::{ PlaybookRun, package::{PackageModule as Package, PackageState, PackageError}, @@ -25,9 +23,6 @@ impl From for PlaybookError { } fn main() -> Result<(), PlaybookError> { - let facts = Facts::new(); - println!("rustible running system: {}", facts.os.family().as_str()); - let mut playbook = PlaybookRun::new(); let pkg = Package::new() @@ -39,7 +34,7 @@ fn main() -> Result<(), PlaybookError> { let cmd = Command::new() .program("hello") .arg("-g") - .arg("\"Welcome to rustible!\"") + .arg("\"Welcome to rustible!\"") .build(); let res = playbook.run(cmd)?; diff --git a/src/main.rs b/src/main.rs index 9ff466b..33f533d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,28 @@ -use rustible::Facts; +use ron::value::Value; +use serde::{Serialize, Deserialize}; +use std::env::args; + +use rustible::Facts; use rustible::modules::{ - Module, PlaybookRun, - package::{PackageModule as Package, PackageState, PackageError}, - command::{CommandModule as Command, CommandError}, + Module, PlaybookRun, ModuleSetup, + package::{PackageModule as Package, PackageState, PackageError, PackageArgs}, + command::{CommandModule as Command, CommandError, CommandArgs}, }; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct DeclarativePlaybook { + modules: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct DeclarativeModule { + name: String, + module: String, + args: Value, +} + #[derive(Clone, Debug, serde::Serialize)] pub enum PlaybookError { Command(CommandError), @@ -26,48 +43,35 @@ impl From for PlaybookError { fn main() -> Result<(), PlaybookError> { 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 mut cli_args = args(); + let test_playbook = cli_args.nth(1).expect("First argument should be playbook.ron"); + let test_shell = cli_args.nth(0).expect("Second argument should be test.sh"); - let pkg = Package::new() - .name("hello") - .state(PackageState::Present) - .build(); - playbook.run(pkg)?; + let test_playbook_content = std::fs::read_to_string(&test_playbook).unwrap(); - let pkg = Package::new() - .name("hello") - .state(PackageState::Absent) - .build(); - playbook.run(pkg)?; + let playbook_ron: DeclarativePlaybook = ron::from_str(&test_playbook_content).unwrap(); - let pkg = Package::new() - .name(vec!("hello", "sl")) - .state(PackageState::Present) - .build(); - playbook.run(pkg)?; - - let pkg = Package::new() - .name(&[ "hello", "sl" ]) - .state(PackageState::Absent) - .build(); - playbook.run(pkg)?; - playbook.print_json_pretty(); + for task in playbook_ron.modules { + println!("{}", task.name); + match task.module.as_str() { + "command" => { + let args: CommandArgs = task.args.into_rust().unwrap(); + let module = args.with_facts(&facts); + playbook.run(module)?; + //println!("{:?}", module); + }, "package" => { + let args: PackageArgs = task.args.into_rust().unwrap(); + let module = args.with_facts(&facts); + playbook.run(module)?; + //println!("{:?}", module); + }, _ => { + unimplemented!(); + } + } + } Ok(()) -} \ No newline at end of file +} diff --git a/src/modules/command/builder.rs b/src/modules/command/builder.rs index 4354bc8..46fa73c 100644 --- a/src/modules/command/builder.rs +++ b/src/modules/command/builder.rs @@ -9,11 +9,11 @@ use crate::utils::cmd::Cmd; pub struct NoCmd; pub struct NoCondition; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct CreatesCondition(pub PathBuf); -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RemovesCondition(pub PathBuf); -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct BothConditions(pub CreatesCondition, pub RemovesCondition); /// Build a [`CommandModule`]. diff --git a/src/modules/command/condition.rs b/src/modules/command/condition.rs index b4a2caf..6563ff5 100644 --- a/src/modules/command/condition.rs +++ b/src/modules/command/condition.rs @@ -1,6 +1,6 @@ use super::builder::{NoCondition, CreatesCondition, RemovesCondition, BothConditions}; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub enum SomeCondition { #[serde(rename="creates")] Creates(CreatesCondition), diff --git a/src/modules/command/mod.rs b/src/modules/command/mod.rs index 22c314b..4482856 100644 --- a/src/modules/command/mod.rs +++ b/src/modules/command/mod.rs @@ -12,13 +12,24 @@ 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)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct CommandArgs { #[serde(flatten)] pub args: Cmd, pub condition: Option, } +impl ModuleSetup for CommandArgs { + fn with_facts(self, _facts: &Facts) -> CommandModule { + CommandModule { + cmd: self.args, + condition: self.condition, + + } + } +} + +#[derive(Debug)] pub struct CommandModule { cmd: Cmd, condition: Option, diff --git a/src/modules/package/apt.rs b/src/modules/package/apt.rs index 98cd9f4..755641b 100644 --- a/src/modules/package/apt.rs +++ b/src/modules/package/apt.rs @@ -1,6 +1,8 @@ +use rayon::prelude::*; + use crate::utils::cmd::{Cmd, CmdOutput}; -use super::{PackageError, PackageList, PackageManager}; +use super::{PackageError, PackageList, PackageManager, IntoPackageList}; /// This structure is mostly used as an indirection for testing purposes. /// But you can use it to build your own apt commands. @@ -40,6 +42,14 @@ impl AptCmd { #[derive(Debug)] pub struct AptManager; +impl AptManager { + fn is_installed_list(&self, list: &PackageList) -> Result { + let res = Cmd::new("dpkg").arg("-s").args(list.list()).run()?; + + Ok(res.success) + } +} + impl PackageManager for AptManager { fn name(&self) -> &'static str { "apt" @@ -54,7 +64,26 @@ impl PackageManager for AptManager { Ok(()) } + fn is_installed(&self, pkg: &str) -> Result { + let res = Cmd::new("dpkg").arg("-s").arg(pkg).run()?; + + Ok(res.success) + } + fn install(&self, list: PackageList) -> Result<(), PackageError> { + // Filter out already installed packages + // let list: Vec = list.list().par_iter().filter_map(|pkg| { + // if ! self.is_installed(&pkg).unwrap() { + // Some(pkg.to_string()) + // } else { + // None + // } + // }).collect(); + if self.is_installed_list(&list)? { + return Ok(()); + } + + //let res = AptCmd::install(list.into_package_list()).run()?; let res = AptCmd::install(list).run()?; if ! res.success { diff --git a/src/modules/package/list.rs b/src/modules/package/list.rs index 1a3e7b7..96bacee 100644 --- a/src/modules/package/list.rs +++ b/src/modules/package/list.rs @@ -1,8 +1,11 @@ -use serde::Serialize; +use serde::{Serialize, Deserialize}; -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +use crate::utils::serde::string_or_seq_string; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[serde(transparent)] pub struct PackageList { + #[serde(deserialize_with = "string_or_seq_string")] pub list: Vec, } diff --git a/src/modules/package/mod.rs b/src/modules/package/mod.rs index 67b9143..42c80ea 100644 --- a/src/modules/package/mod.rs +++ b/src/modules/package/mod.rs @@ -1,4 +1,4 @@ -use serde::Serialize; +use serde::{Serialize, Deserialize, Deserializer}; use crate::{Module, ModuleSetup}; use crate::facts::{Facts, os::OsFamily}; @@ -19,12 +19,25 @@ pub use builder::{PackageArgsBuilder, NoManager}; pub type PackageStatus = (); -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PackageArgs { name: PackageList, state: PackageState, } +impl ModuleSetup>, PackageArgs, (), PackageError> for PackageArgs { + fn with_facts(self, facts: &Facts) -> PackageModule> { + let manager: Box = match facts.os.family() { + OsFamily::Debian => Box::new(AptManager), + OsFamily::Archlinux => Box::new(PacmanManager), + }; + + PackageModule { + args: self, + manager, + } + } +} #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub enum PackageState { Present, @@ -32,6 +45,21 @@ pub enum PackageState { Latest, } +impl<'de> Deserialize<'de> for PackageState { + fn deserialize(de: D) -> Result + where + D: Deserializer<'de>, + { + let variant = String::deserialize(de)?; + Ok(match variant.as_str() { + "present" => PackageState::Present, + "absent" => PackageState::Absent, + "latest" => PackageState::Latest, + _other => unimplemented!(), + }) + } +} + #[derive(Debug, PartialEq, Eq)] pub struct PackageModule { manager: Manager, @@ -78,6 +106,12 @@ impl ModuleSetup>, PackageArgs, (), Packag } } +// Stupid impl just for tests +impl ModuleSetup>, PackageArgs, (), PackageError> for PackageModule> { + fn with_facts(self, _facts: &Facts) -> PackageModule> { + self + } +} impl PackageModule { pub fn new() -> PackageArgsBuilder { @@ -100,6 +134,7 @@ impl From for PackageError { pub trait PackageManager: std::fmt::Debug { fn name(&self) -> &'static str; fn update(&self) -> Result<(), PackageError>; + fn is_installed(&self, pkg: &str) -> Result; fn install(&self, list: PackageList) -> Result<(), PackageError>; fn remove(&self, list: PackageList) -> Result<(), PackageError>; } diff --git a/src/modules/package/pacman.rs b/src/modules/package/pacman.rs index c4070ba..3569477 100644 --- a/src/modules/package/pacman.rs +++ b/src/modules/package/pacman.rs @@ -20,6 +20,12 @@ impl PacmanCmd { cmd } + pub fn is_installed(pkg: &str) -> PacmanCmd { + let mut cmd = Self::new(); + cmd.0 = cmd.0.arg("-Qi").arg(pkg); + cmd + } + pub fn install(list: PackageList) -> PacmanCmd { let mut cmd = Self::new(); cmd.0 = cmd.0.arg("-S").args(list.list()); @@ -56,6 +62,12 @@ impl PackageManager for PacmanManager { Ok(()) } + fn is_installed(&self, pkg: &str) -> Result { + let res = PacmanCmd::is_installed(pkg).run()?; + + Ok(res.success) + } + fn install(&self, list: PackageList) -> Result<(), PackageError> { let res = PacmanCmd::install(list).run()?; diff --git a/src/utils/cmd.rs b/src/utils/cmd.rs index 71c7320..1cde0c3 100644 --- a/src/utils/cmd.rs +++ b/src/utils/cmd.rs @@ -3,13 +3,17 @@ use duct::cmd as duct_cmd; use std::collections::HashMap; use std::path::PathBuf; -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Cmd { - pub program: String, - pub args: Vec, - pub stdin: Option, - pub chdir: Option, - pub env: HashMap, + program: String, + #[serde(default)] + args: Vec, + #[serde(default)] + stdin: Option, + #[serde(default)] + chdir: Option, + #[serde(default)] + env: HashMap, } impl Cmd { diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 52958ec..d978f72 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1 +1,2 @@ pub mod cmd; +pub mod serde; diff --git a/src/utils/serde.rs b/src/utils/serde.rs new file mode 100644 index 0000000..257ff6a --- /dev/null +++ b/src/utils/serde.rs @@ -0,0 +1,34 @@ +use serde::de::{Deserialize, Deserializer, SeqAccess, Visitor}; +use serde::de::value::SeqAccessDeserializer; + +use std::fmt; +use std::marker::PhantomData; + +// https://stackoverflow.com/questions/41151080/deserialize-a-json-string-or-array-of-strings-into-a-vec +pub fn string_or_seq_string<'de, D>(deserializer: D) -> Result, D::Error> + where D: Deserializer<'de> +{ + struct StringOrVec(PhantomData>); + + impl<'de> Visitor<'de> for StringOrVec { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("string or list of strings") + } + + fn visit_str(self, value: &str) -> Result + where E: serde::de::Error + { + Ok(vec![value.to_owned()]) + } + + fn visit_seq(self, visitor: S) -> Result + where S: SeqAccess<'de> + { + Deserialize::deserialize(SeqAccessDeserializer::new(visitor)) + } + } + + deserializer.deserialize_any(StringOrVec(PhantomData)) +} \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100755 index 0000000..b313294 --- /dev/null +++ b/test.sh @@ -0,0 +1,91 @@ +#! /usr/bin/env bash + +help() { + echo "test.sh [SERVER]" + echo "Start the rustible test suite on a certain machine (localhost by default)" + echo "Requires lxc-ramdisk to be installed: https://kl.netlib.re/gitea/selfhoster1312/lxc-ramdisk" +} + +build() { + if cargo build --release 2>&1 >/dev/null; then + echo "$(pwd)"/target/release/rustible + ret=0 + else + echo "BUILD FAILED." + ret=1 + fi + + return $ret +} + +gen_rand_name() { + echo rustible-"$RANDOM" +} + +execute_localhost() { + while true; do + TEST_NAME="$(gen_rand_name)" + TEST_PATH=/tmp/"$TEST_NAME" + if [ ! -e "$TEST_PATH" ]; then + break + fi + done + + echo "Start test $TEST_NAME" + cp target/release/rustible "$TEST_PATH" +} + +execute_on_server() { + SERVER="$1" + if ssh "$SERVER" true >/dev/null 2>&1; then + echo "Connection to "$1" successful" + else + echo "ERROR: Connection failed to "$1"" + return 1 + fi + + while true; do + TEST_NAME="$(gen_rand_name)" + TEST_PATH=/tmp/"$TEST_NAME" + if ! ssh "$1" ls "$TEST_PATH" >/dev/null 2>&1; then + break + fi + done + + echo "Start test "$TEST_NAME" ("$TEST_PATH")" + if ! scp target/release/rustible "$SERVER":"$TEST_PATH"; then + echo "ERROR: Copy to "$SERVER" failed" + return 1 + fi +} + +# Move to crate directory +OLDDIR="$(pwd)" +cd "$(dirname "$0")" + +# Figure out rustible test binary path +BIN_PATH="$(build)" +if [ ! $? -eq 0 ]; then + # Compilation failed, abort + echo "$BIN_PATH" + exit 1 +fi + +if [[ "$1" = "" ]]; then + execute_localhost + ret=$? +else + case "$1" in + "help" | "-h" | "--help") + help + ret=0 + ;; + *) + execute_on_server "$1" + ret=$? + ;; + esac +fi + +cd "$OLDDIR" +exit $ret diff --git a/tests/hello/playbook.ron b/tests/hello/playbook.ron new file mode 100644 index 0000000..0aa370e --- /dev/null +++ b/tests/hello/playbook.ron @@ -0,0 +1,19 @@ +( +modules: [ + ( + name: "install hello", + module: "package", + args: ( + name: [ "hello", "cmake", "firefox-esr", "libreoffice", "make", "gcc", "zstd", "zlib1g", "zlib1g-dev", "zenity", "zenity-common", "yt-dlp", "yelp", "yelp-xsl", "xz-utils", "xserver-xorg", "xtrans-dev" ], + state: "present", + ) + ), ( + name: "say hello", + module: "command", + args: ( + program: "hello", + args: [ "-g", "welcome to rustible" ], + ), + ) +] +) diff --git a/tests/hello/test.sh b/tests/hello/test.sh new file mode 100644 index 0000000..07ca47e --- /dev/null +++ b/tests/hello/test.sh @@ -0,0 +1,7 @@ +#! /usr/bin/env bash + +# Check that the playbook run returned "welcome to rustible" +echo "$STDOUT" | tail -n 3 | grep -P "\^"Welcome to rustible!\"" + +# Check that hello is effectively installed +hello -g "test" | grep -P "^test$"