Compare commits

..

5 Commits

Author SHA1 Message Date
selfhoster1312 89ef66a694 TRUC 2023-10-05 17:58:41 +02:00
selfhoster1312 7d9e3d63b7 Add hello.rs Hello World example 2023-04-21 17:25:31 +02:00
selfhoster1312 d2ef322761 Rename SpecificPackageManager trait PackageManager, add some unit tests 2023-04-21 15:13:03 +02:00
selfhoster1312 1245db813b Add new Package examples to main
Print debug line with module arguments when run starts
Make apt package manager non-interactive
Move PackageList to dedicated module and make more impls
utils::Cmd now can take environment variables
2023-04-20 18:31:45 +02:00
selfhoster1312 f26f1c1bf2 Add Package example to main
Add remove method to pacman/apt package managers
2023-04-20 17:31:45 +02:00
21 changed files with 1034 additions and 193 deletions
Generated
+142
View File
@@ -11,6 +11,73 @@ dependencies = [
"memchr", "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]] [[package]]
name = "ctor" name = "ctor"
version = "0.2.0" version = "0.2.0"
@@ -39,6 +106,12 @@ dependencies = [
"shared_child", "shared_child",
] ]
[[package]]
name = "either"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]] [[package]]
name = "erased-serde" name = "erased-serde"
version = "0.3.25" version = "0.3.25"
@@ -65,6 +138,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 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]] [[package]]
name = "inventory" name = "inventory"
version = "0.3.5" version = "0.3.5"
@@ -93,6 +175,25 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.17.1" version = "1.17.1"
@@ -127,6 +228,28 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "regex" name = "regex"
version = "1.7.3" version = "1.7.3"
@@ -144,12 +267,25 @@ version = "0.6.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 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]] [[package]]
name = "rustible" name = "rustible"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"duct", "duct",
"rayon",
"regex", "regex",
"ron",
"serde", "serde",
"serde_json", "serde_json",
"snafu", "snafu",
@@ -162,6 +298,12 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.160" version = "1.0.160"
+2
View File
@@ -15,6 +15,8 @@ typetag = "0.2"
# Error management # Error management
#thiserror = "1" #thiserror = "1"
snafu = "0.7" snafu = "0.7"
ron = "0.8"
rayon = "1.7"
[lib] [lib]
name = "rustible" name = "rustible"
+2
View File
@@ -1 +1,3 @@
WIP ansible-like system for sysadmin in rust WIP ansible-like system for sysadmin in rust
TODO: finish test.sh for testing things
+6
View File
@@ -0,0 +1,6 @@
use rustible::Facts;
fn main() {
let facts = Facts::new();
println!("rustible running system: {}", facts.os.family().as_str());
}
+44
View File
@@ -0,0 +1,44 @@
use rustible::modules::{
PlaybookRun,
package::{PackageModule as Package, PackageState, PackageError},
command::{CommandModule as Command, CommandError},
};
#[derive(Clone, Debug, serde::Serialize)]
pub enum PlaybookError {
Command(CommandError),
Package(PackageError),
}
impl From<CommandError> for PlaybookError {
fn from(e: CommandError) -> PlaybookError {
PlaybookError::Command(e)
}
}
impl From<PackageError> for PlaybookError {
fn from(e: PackageError) -> PlaybookError {
PlaybookError::Package(e)
}
}
fn main() -> Result<(), PlaybookError> {
let mut playbook = PlaybookRun::new();
let pkg = Package::new()
.name("hello")
.state(PackageState::Present)
.build();
playbook.run(pkg)?;
let cmd = Command::new()
.program("hello")
.arg("-g")
.arg("\"Welcome to rustible!\"")
.build();
let res = playbook.run(cmd)?;
println!("STDOUT:\n{}", res.stdout().unwrap());
Ok(())
}
+66 -17
View File
@@ -1,28 +1,77 @@
use rustible::Facts; use ron::value::Value;
use rustible::modules::{PlaybookRun, Module, Command}; use serde::{Serialize, Deserialize};
fn main() -> Result<(), rustible::modules::command::CommandError> { use std::env::args;
use rustible::Facts;
use rustible::modules::{
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<DeclarativeModule>,
}
#[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),
Package(PackageError),
}
impl From<CommandError> for PlaybookError {
fn from(e: CommandError) -> PlaybookError {
PlaybookError::Command(e)
}
}
impl From<PackageError> for PlaybookError {
fn from(e: PackageError) -> PlaybookError {
PlaybookError::Package(e)
}
}
fn main() -> Result<(), PlaybookError> {
let facts = Facts::new(); let facts = Facts::new();
println!("Hello, world! Running system {}", facts.os.family().as_str());
let mut playbook = PlaybookRun::new(); let mut playbook = PlaybookRun::new();
let cmd = Command::new().program("echo").args(&vec!("lol".to_string())).build(); let mut cli_args = args();
let res = cmd.run()?; let test_playbook = cli_args.nth(1).expect("First argument should be playbook.ron");
println!("{}", res.success()); let test_shell = cli_args.nth(0).expect("Second argument should be test.sh");
let cmd = Command::new().program("echo").args(&vec!("lol".to_string())).build(); let test_playbook_content = std::fs::read_to_string(&test_playbook).unwrap();
playbook.run(cmd)?;
let cmd = Command::new() let playbook_ron: DeclarativePlaybook = ron::from_str(&test_playbook_content).unwrap();
.creates("/tmp/lol")
.program("touch")
.arg("/tmp/lol")
.build();
playbook.run(cmd)?;
//let res = cmd.run
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(()) Ok(())
} }
+3 -3
View File
@@ -9,11 +9,11 @@ use crate::utils::cmd::Cmd;
pub struct NoCmd; pub struct NoCmd;
pub struct NoCondition; pub struct NoCondition;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CreatesCondition(pub PathBuf); pub struct CreatesCondition(pub PathBuf);
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RemovesCondition(pub PathBuf); pub struct RemovesCondition(pub PathBuf);
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BothConditions(pub CreatesCondition, pub RemovesCondition); pub struct BothConditions(pub CreatesCondition, pub RemovesCondition);
/// Build a [`CommandModule`]. /// Build a [`CommandModule`].
+1 -1
View File
@@ -1,6 +1,6 @@
use super::builder::{NoCondition, CreatesCondition, RemovesCondition, BothConditions}; use super::builder::{NoCondition, CreatesCondition, RemovesCondition, BothConditions};
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum SomeCondition { pub enum SomeCondition {
#[serde(rename="creates")] #[serde(rename="creates")]
Creates(CreatesCondition), Creates(CreatesCondition),
+26 -1
View File
@@ -12,13 +12,24 @@ use crate::{Module, ModuleSetup, Facts};
/// A copy of the argument passed to the [`CommandBuilder`], /// A copy of the argument passed to the [`CommandBuilder`],
/// as returned serialized in the task run output /// as returned serialized in the task run output
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CommandArgs { pub struct CommandArgs {
#[serde(flatten)] #[serde(flatten)]
pub args: Cmd, pub args: Cmd,
pub condition: Option<SomeCondition>, pub condition: Option<SomeCondition>,
} }
impl ModuleSetup<CommandModule, CommandArgs, CommandStatus, CommandError> for CommandArgs {
fn with_facts(self, _facts: &Facts) -> CommandModule {
CommandModule {
cmd: self.args,
condition: self.condition,
}
}
}
#[derive(Debug)]
pub struct CommandModule { pub struct CommandModule {
cmd: Cmd, cmd: Cmd,
condition: Option<SomeCondition>, condition: Option<SomeCondition>,
@@ -92,6 +103,20 @@ impl CommandStatus {
Self::Skipped(_c) => true, Self::Skipped(_c) => true,
} }
} }
pub fn stdout(&self) -> Option<String> {
match self {
Self::Done(ret) => Some(ret.stdout.to_string()),
Self::Skipped(_c) => None,
}
}
pub fn stderr(&self) -> Option<String> {
match self {
Self::Done(ret) => Some(ret.stdout.to_string()),
Self::Skipped(_c) => None,
}
}
} }
/// Return of a Command that was effectively run /// Return of a Command that was effectively run
+2 -1
View File
@@ -79,6 +79,7 @@ impl PlaybookRun {
let module_name = cmd.module_name().to_string(); let module_name = cmd.module_name().to_string();
let args = cmd.serialize_args(); let args = cmd.serialize_args();
println!("Running module {} with args:{:#?}\n\n", &module_name, &args);
let res = cmd.run(); let res = cmd.run();
@@ -129,4 +130,4 @@ pub trait Module<A: Serialize, S: Serialize, E: Serialize> {
/// Declare module types /// Declare module types
pub use command::CommandModule as Command; pub use command::CommandModule as Command;
pub use package::{PackageModule as Package, PackageState};
+174 -8
View File
@@ -1,17 +1,62 @@
use crate::utils::cmd::Cmd; use rayon::prelude::*;
use super::{PackageError, PackageList, SpecificPackageManager}; use crate::utils::cmd::{Cmd, CmdOutput};
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.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AptCmd(pub Cmd);
impl AptCmd {
pub fn new() -> AptCmd {
AptCmd(
Cmd::new("apt").env("DEBIAN_FRONTEND", "noninteractive")
)
}
pub fn update() -> AptCmd {
let mut cmd = Self::new();
cmd.0 = cmd.0.arg("update");
cmd
}
pub fn install(list: PackageList) -> AptCmd {
let mut cmd = Self::new();
cmd.0 = cmd.0.arg("install").args(list.list());
cmd
}
pub fn remove(list: PackageList) -> AptCmd {
let mut cmd = Self::new();
cmd.0 = cmd.0.arg("remove").args(list.list());
cmd
}
pub fn run(self) -> Result<CmdOutput, std::io::Error> {
self.0.run()
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct DebianPackageManager; pub struct AptManager;
impl SpecificPackageManager for DebianPackageManager { impl AptManager {
fn is_installed_list(&self, list: &PackageList) -> Result<bool, PackageError> {
let res = Cmd::new("dpkg").arg("-s").args(list.list()).run()?;
Ok(res.success)
}
}
impl PackageManager for AptManager {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"apt" "apt"
} }
fn update(&self) -> Result<(), PackageError> { fn update(&self) -> Result<(), PackageError> {
let res = Cmd::new("apt").arg("update").run()?; let res = AptCmd::update().run()?;
if ! res.success { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@@ -19,8 +64,27 @@ impl SpecificPackageManager for DebianPackageManager {
Ok(()) Ok(())
} }
fn is_installed(&self, pkg: &str) -> Result<bool, PackageError> {
let res = Cmd::new("dpkg").arg("-s").arg(pkg).run()?;
Ok(res.success)
}
fn install(&self, list: PackageList) -> Result<(), PackageError> { fn install(&self, list: PackageList) -> Result<(), PackageError> {
let res = Cmd::new("apt").arg("install").args(list.list()).run()?; // Filter out already installed packages
// let list: Vec<String> = 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 { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@@ -28,7 +92,109 @@ impl SpecificPackageManager for DebianPackageManager {
Ok(()) Ok(())
} }
fn remove(&self, _list: PackageList) -> Result<(), PackageError> { fn remove(&self, list: PackageList) -> Result<(), PackageError> {
todo!() let res = AptCmd::remove(list).run()?;
if ! res.success {
return Err(PackageError::CmdFail(res));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::IntoPackageList;
use std::collections::HashMap;
#[test]
pub fn update_cmd() {
let cmd = AptCmd::update();
let mut env: HashMap<String, String> = HashMap::new();
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
assert_eq!(
cmd.0,
Cmd {
program: "apt".to_string(),
args: vec!("update".to_string()),
stdin: None,
chdir: None,
env,
}
)
}
#[test]
pub fn install_cmd() {
let cmd = AptCmd::install(vec!("sl").into_package_list());
let mut env: HashMap<String, String> = HashMap::new();
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
assert_eq!(
cmd.0,
Cmd {
program: "apt".to_string(),
args: vec!("install", "sl").iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env,
}
)
}
#[test]
pub fn install_cmd_multi() {
let cmd = AptCmd::install(vec!("sl", "hello").into_package_list());
let mut env: HashMap<String, String> = HashMap::new();
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
assert_eq!(
cmd.0,
Cmd {
program: "apt".to_string(),
args: vec!("install", "sl", "hello").iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env,
}
)
}
#[test]
pub fn remove_cmd() {
let cmd = AptCmd::remove(vec!("sl").into_package_list());
let mut env: HashMap<String, String> = HashMap::new();
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
assert_eq!(
cmd.0,
Cmd {
program: "apt".to_string(),
args: vec!("remove", "sl").iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env,
}
)
}
#[test]
pub fn remove_cmd_multi() {
let cmd = AptCmd::remove(vec!("sl", "hello").into_package_list());
let mut env: HashMap<String, String> = HashMap::new();
env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string());
assert_eq!(
cmd.0,
Cmd {
program: "apt".to_string(),
args: vec!("remove", "sl", "hello").iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env,
}
)
} }
} }
+24 -11
View File
@@ -1,6 +1,5 @@
use super::{PackageArgs, IntoPackageList, PackageList, PackageState, PackageModule, PackageStatus, PackageError, SpecificPackageManager}; use super::{PackageArgs, IntoPackageList, PackageList, PackageState, PackageModule, PackageStatus, PackageError, PackageManager};
use super::apt::DebianPackageManager; use super::{apt::AptManager, pacman::PacmanManager};
use super::pacman::ArchlinuxPackageManager;
use std::boxed::Box; use std::boxed::Box;
use crate::Module; use crate::Module;
@@ -50,7 +49,7 @@ impl<Packages, Manager> PackageArgsBuilder<Packages, NoState, Manager> {
} }
impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> { impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> {
pub fn with(self, manager: impl SpecificPackageManager + 'static) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> { pub fn with(self, manager: impl PackageManager + 'static) -> PackageArgsBuilder<Packages, State, Box<dyn PackageManager>> {
let Self { name, state, .. } = self; let Self { name, state, .. } = self;
PackageArgsBuilder { PackageArgsBuilder {
@@ -60,21 +59,21 @@ impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> {
} }
} }
pub fn with_apt(self) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> { pub fn with_apt(self) -> PackageArgsBuilder<Packages, State, Box<dyn PackageManager>> {
let Self { name, state, .. } = self; let Self { name, state, .. } = self;
PackageArgsBuilder { PackageArgsBuilder {
name, name,
state, state,
manager: Box::new(DebianPackageManager), manager: Box::new(AptManager),
} }
} }
pub fn with_pacman(self) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> { pub fn with_pacman(self) -> PackageArgsBuilder<Packages, State, Box<dyn PackageManager>> {
let Self { name, state, .. } = self; let Self { name, state, .. } = self;
PackageArgsBuilder { PackageArgsBuilder {
name, name,
state, state,
manager: Box::new(ArchlinuxPackageManager), manager: Box::new(PacmanManager),
} }
} }
} }
@@ -82,14 +81,14 @@ impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> {
// We can only RUN the args when a package manager has been set. Alternatively, // 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. // when called inside a playbook, the package manager can be populated automatically from facts.
impl PackageArgsBuilder<PackageList, PackageState, Box<dyn SpecificPackageManager>> { impl PackageArgsBuilder<PackageList, PackageState, Box<dyn PackageManager>> {
pub fn run(self) -> Result<PackageStatus, PackageError> { pub fn run(self) -> Result<PackageStatus, PackageError> {
self.build().run() self.build().run()
} }
} }
impl PackageArgsBuilder<PackageList, PackageState, Box<dyn SpecificPackageManager>> { impl PackageArgsBuilder<PackageList, PackageState, Box<dyn PackageManager>> {
pub fn build(self) -> PackageModule<Box<dyn SpecificPackageManager>> { pub fn build(self) -> PackageModule<Box<dyn PackageManager>> {
let Self { name, state, manager, .. } = self; let Self { name, state, manager, .. } = self;
PackageModule { PackageModule {
@@ -101,3 +100,17 @@ impl PackageArgsBuilder<PackageList, PackageState, Box<dyn SpecificPackageManage
} }
} }
} }
impl PackageArgsBuilder<PackageList, PackageState, NoManager> {
pub fn build(self) -> PackageModule<NoManager> {
let Self { name, state, .. } = self;
PackageModule {
args: PackageArgs {
name,
state,
},
manager: NoManager,
}
}
}
+63 -89
View File
@@ -1,110 +1,84 @@
use serde::Serialize; use serde::{Serialize, Deserialize};
use crate::facts::{Facts, os::OsFamily}; use crate::utils::serde::string_or_seq_string;
use crate::utils::cmd::CmdOutput;
use std::boxed::Box; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub mod pacman; pub struct PackageList {
use pacman::ArchlinuxPackageManager; #[serde(deserialize_with = "string_or_seq_string")]
pub mod apt; pub list: Vec<String>,
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)] impl PackageList {
pub enum PackageState { pub fn list(&self) -> & [ String ] {
Present, &self.list
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 { pub fn add<T: IntoPackageList>(self, add: T) -> PackageList {
"package" let Self { mut list } = self;
}
}
impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn SpecificPackageManager>> { let extend_with = add.into_package_list();
fn run(&self) -> Result<(), PackageError> { list.extend(extend_with.list);
Ok(())
}
}
PackageList {
impl Module<PackageArgs, (), PackageError> for PackageModule<NoManager> { list
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,
} }
} }
} }
/// Turn a stringy value or list of values into an actual package list.
/// Only implemented for &str and &str/String slices because apparently
/// there may be a future implementation that turns a slice of stringy values
/// into a stringy value, resulting in conflicting implementations.
/// https://stackoverflow.com/questions/63136970/how-do-i-work-around-the-upstream-crates-may-add-a-new-impl-of-trait-error
pub trait IntoPackageList {
fn into_package_list(self) -> PackageList;
}
impl PackageModule { impl IntoPackageList for &str {
pub fn new() -> PackageArgsBuilder { fn into_package_list(self) -> PackageList {
PackageArgsBuilder::new() PackageList {
} list: vec!(self.to_string()),
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)] impl IntoPackageList for String {
pub enum PackageError { fn into_package_list(self) -> PackageList {
IoError(String), PackageList {
CmdFail(CmdOutput), list: vec!(self),
} }
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 { impl<const C: usize> IntoPackageList for & [ &str; C ] {
fn name(&self) -> &'static str; fn into_package_list(self) -> PackageList {
fn update(&self) -> Result<(), PackageError>; PackageList {
fn install(&self, list: PackageList) -> Result<(), PackageError>; list: self.iter().map(|x| x.to_string()).collect(),
fn remove(&self, list: PackageList) -> Result<(), PackageError>; }
}
}
impl<const C: usize> IntoPackageList for & [ String; C ] {
fn into_package_list(self) -> PackageList {
PackageList {
list: self.to_vec(),
}
}
}
impl IntoPackageList for Vec<&str> {
fn into_package_list(self) -> PackageList {
PackageList {
list: self.iter().map(|x| x.to_string()).collect(),
}
}
}
impl IntoPackageList for Vec<String> {
fn into_package_list(self) -> PackageList {
PackageList {
list: self,
}
}
} }
+128 -46
View File
@@ -1,4 +1,4 @@
use serde::Serialize; use serde::{Serialize, Deserialize, Deserializer};
use crate::{Module, ModuleSetup}; use crate::{Module, ModuleSetup};
use crate::facts::{Facts, os::OsFamily}; use crate::facts::{Facts, os::OsFamily};
@@ -6,40 +6,67 @@ use crate::utils::cmd::CmdOutput;
use std::boxed::Box; use std::boxed::Box;
pub mod list;
pub use list::{PackageList, IntoPackageList};
pub mod pacman; pub mod pacman;
use pacman::ArchlinuxPackageManager; use pacman::PacmanManager;
pub mod apt; pub mod apt;
use apt::DebianPackageManager; use apt::AptManager;
pub mod builder; pub mod builder;
pub use builder::PackageArgsBuilder; pub use builder::{PackageArgsBuilder, NoManager};
pub type PackageStatus = (); pub type PackageStatus = ();
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PackageArgs { pub struct PackageArgs {
name: PackageList, name: PackageList,
state: PackageState, state: PackageState,
} }
#[derive(Clone, Debug, Serialize)] impl ModuleSetup<PackageModule<Box<dyn PackageManager>>, PackageArgs, (), PackageError> for PackageArgs {
fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn PackageManager>> {
let manager: Box<dyn PackageManager> = 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 { pub enum PackageState {
Present, Present,
Absent, Absent,
Latest, Latest,
} }
#[derive(Debug)] impl<'de> Deserialize<'de> for PackageState {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
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> { pub struct PackageModule<Manager> {
manager: Manager, manager: Manager,
args: PackageArgs, args: PackageArgs,
} }
impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn PackageManager>> {
#[derive(Debug)]
pub struct NoManager;
impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn SpecificPackageManager>> {
fn serialize_args(&self) -> serde_json::Value { fn serialize_args(&self) -> serde_json::Value {
serde_json::to_value(&self.args).unwrap() serde_json::to_value(&self.args).unwrap()
} }
@@ -49,17 +76,27 @@ impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn SpecificPac
} }
fn run(self) -> Result<(), PackageError> { fn run(self) -> Result<(), PackageError> {
Ok(()) let packages = self.args.name;
match &self.args.state {
PackageState::Present => {
self.manager.install(packages)
}, PackageState::Absent => {
self.manager.remove(packages)
}, PackageState::Latest => {
unimplemented!()
}
}
} }
} }
impl ModuleSetup<PackageModule<Box<dyn SpecificPackageManager>>, PackageArgs, (), PackageError> for PackageModule<NoManager> { impl ModuleSetup<PackageModule<Box<dyn PackageManager>>, PackageArgs, (), PackageError> for PackageModule<NoManager> {
fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn SpecificPackageManager>> { fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn PackageManager>> {
let Self { args, .. } = self; let Self { args, .. } = self;
let manager: Box<dyn SpecificPackageManager> = match facts.os.family() { let manager: Box<dyn PackageManager> = match facts.os.family() {
OsFamily::Debian => Box::new(DebianPackageManager), OsFamily::Debian => Box::new(AptManager),
OsFamily::Archlinux => Box::new(ArchlinuxPackageManager), OsFamily::Archlinux => Box::new(PacmanManager),
}; };
PackageModule { PackageModule {
@@ -69,6 +106,12 @@ impl ModuleSetup<PackageModule<Box<dyn SpecificPackageManager>>, PackageArgs, ()
} }
} }
// Stupid impl just for tests
impl ModuleSetup<PackageModule<Box<dyn PackageManager>>, PackageArgs, (), PackageError> for PackageModule<Box<dyn PackageManager>> {
fn with_facts(self, _facts: &Facts) -> PackageModule<Box<dyn PackageManager>> {
self
}
}
impl PackageModule<NoManager> { impl PackageModule<NoManager> {
pub fn new() -> PackageArgsBuilder<builder::NoPackage, builder::NoState, builder::NoManager> { pub fn new() -> PackageArgsBuilder<builder::NoPackage, builder::NoState, builder::NoManager> {
@@ -88,44 +131,83 @@ impl From<std::io::Error> for PackageError {
} }
} }
pub trait SpecificPackageManager: std::fmt::Debug { pub trait PackageManager: std::fmt::Debug {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn update(&self) -> Result<(), PackageError>; fn update(&self) -> Result<(), PackageError>;
fn is_installed(&self, pkg: &str) -> Result<bool, PackageError>;
fn install(&self, list: PackageList) -> Result<(), PackageError>; fn install(&self, list: PackageList) -> Result<(), PackageError>;
fn remove(&self, list: PackageList) -> Result<(), PackageError>; fn remove(&self, list: PackageList) -> Result<(), PackageError>;
} }
#[derive(Clone, Debug, Serialize)] #[cfg(test)]
pub struct PackageList { mod tests {
pub list: Vec<String>, use super::*;
}
impl PackageList { #[test]
pub fn list(&self) -> & [ String ] { fn ensure_present() {
&self.list let pkg = PackageModule::new()
.name("hello")
.state(PackageState::Present)
.with_apt()
.build();
assert_eq!(
pkg.serialize_args(),
serde_json::json!({
"name": vec!("hello"),
"state": PackageState::Present,
})
);
} }
pub fn add<T: IntoPackageList>(self, add: T) -> PackageList { #[test]
let Self { mut list } = self; fn ensure_present_multi() {
let pkg = PackageModule::new()
.name(&[ "hello", "sl" ])
.state(PackageState::Present)
.with_apt()
.build();
let extend_with = add.into_package_list(); assert_eq!(
list.extend(extend_with.list); pkg.serialize_args(),
serde_json::json!({
"name": vec!("hello", "sl"),
"state": PackageState::Present,
})
)
}
PackageList { #[test]
list fn ensure_absent() {
} let pkg = PackageModule::new()
.name("hello")
.state(PackageState::Absent)
.with_apt()
.build();
assert_eq!(
pkg.serialize_args(),
serde_json::json!({
"name": vec!("hello"),
"state": PackageState::Absent,
})
)
}
#[test]
fn ensure_absent_multi() {
let pkg = PackageModule::new()
.name(&[ "hello", "sl" ])
.state(PackageState::Absent)
.with_apt()
.build();
assert_eq!(
pkg.serialize_args(),
serde_json::json!({
"name": vec!("hello", "sl"),
"state": PackageState::Absent,
})
)
} }
} }
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()
}
}
}
+159 -8
View File
@@ -1,17 +1,59 @@
use crate::utils::cmd::Cmd; use crate::utils::cmd::{Cmd, CmdOutput};
use super::{PackageError, PackageList, PackageManager};
/// This structure is mostly used as an indirection for testing purposes.
/// But you can use it to build your own pacman commands.
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct PacmanCmd(pub Cmd);
impl PacmanCmd {
pub fn new() -> PacmanCmd {
PacmanCmd(
Cmd::new("pacman").arg("--noconfirm").arg("--needed")
)
}
pub fn update() -> PacmanCmd {
let mut cmd = Self::new();
cmd.0 = cmd.0.arg("-Sy");
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());
cmd
}
pub fn remove(list: PackageList) -> PacmanCmd {
let mut cmd = Self::new();
cmd.0 = cmd.0.arg("-R").args(list.list());
cmd
}
pub fn run(self) -> Result<CmdOutput, std::io::Error> {
self.0.run()
}
}
use super::{PackageError, PackageList, SpecificPackageManager};
#[derive(Debug)] #[derive(Debug)]
pub struct ArchlinuxPackageManager; pub struct PacmanManager;
impl SpecificPackageManager for ArchlinuxPackageManager { impl PackageManager for PacmanManager {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"pacman" "pacman"
} }
fn update(&self) -> Result<(), PackageError> { fn update(&self) -> Result<(), PackageError> {
let res = Cmd::new("pacman").arg("-Sy").run()?; let res = PacmanCmd::update().run()?;
if ! res.success { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@@ -20,8 +62,14 @@ impl SpecificPackageManager for ArchlinuxPackageManager {
Ok(()) Ok(())
} }
fn is_installed(&self, pkg: &str) -> Result<bool, PackageError> {
let res = PacmanCmd::is_installed(pkg).run()?;
Ok(res.success)
}
fn install(&self, list: PackageList) -> Result<(), PackageError> { fn install(&self, list: PackageList) -> Result<(), PackageError> {
let res = Cmd::new("pacman").arg("-S").args(list.list()).run()?; let res = PacmanCmd::install(list).run()?;
if ! res.success { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@@ -30,7 +78,110 @@ impl SpecificPackageManager for ArchlinuxPackageManager {
Ok(()) Ok(())
} }
fn remove(&self, _list: PackageList) -> Result<(), PackageError> { fn remove(&self, list: PackageList) -> Result<(), PackageError> {
todo!() let res = PacmanCmd::remove(list).run()?;
if ! res.success {
return Err(PackageError::CmdFail(res));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use super::super::IntoPackageList;
use std::collections::HashMap;
#[test]
pub fn update_cmd() {
let cmd = PacmanCmd::update();
let mut args: Vec<&str> = vec!("--noconfirm", "--needed");
args.push("-Sy");
assert_eq!(
cmd.0,
Cmd {
program: "pacman".to_string(),
args: args.iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env: HashMap::new(),
}
)
}
#[test]
pub fn install_cmd() {
let cmd = PacmanCmd::install(vec!("sl").into_package_list());
let mut args: Vec<&str> = vec!("--noconfirm", "--needed");
args.extend(& [ "-S", "sl" ]);
assert_eq!(
cmd.0,
Cmd {
program: "pacman".to_string(),
args: args.iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env: HashMap::new(),
}
)
}
#[test]
pub fn install_cmd_multi() {
let cmd = PacmanCmd::install(vec!("sl", "hello").into_package_list());
let mut args: Vec<&str> = vec!("--noconfirm", "--needed");
args.extend(& [ "-S", "sl", "hello" ]);
assert_eq!(
cmd.0,
Cmd {
program: "pacman".to_string(),
args: args.iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env: HashMap::new(),
}
)
}
#[test]
pub fn remove_cmd() {
let cmd = PacmanCmd::remove(vec!("sl").into_package_list());
let mut args: Vec<&str> = vec!("--noconfirm", "--needed");
args.extend(& [ "-R", "sl" ]);
assert_eq!(
cmd.0,
Cmd {
program: "pacman".to_string(),
args: args.iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env: HashMap::new(),
}
)
}
#[test]
pub fn remove_cmd_multi() {
let cmd = PacmanCmd::remove(vec!("sl", "hello").into_package_list());
let mut args: Vec<&str> = vec!("--noconfirm", "--needed");
args.extend(& [ "-R", "sl", "hello" ]);
assert_eq!(
cmd.0,
Cmd {
program: "pacman".to_string(),
args: args.iter().map(|x| x.to_string()).collect(),
stdin: None,
chdir: None,
env: HashMap::new(),
}
)
} }
} }
+39 -7
View File
@@ -1,13 +1,19 @@
use duct::cmd as duct_cmd; use duct::cmd as duct_cmd;
use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cmd { pub struct Cmd {
program: String, program: String,
#[serde(default)]
args: Vec<String>, args: Vec<String>,
#[serde(default)]
stdin: Option<String>, stdin: Option<String>,
#[serde(default)]
chdir: Option<PathBuf>, chdir: Option<PathBuf>,
#[serde(default)]
env: HashMap<String, String>,
} }
impl Cmd { impl Cmd {
@@ -17,11 +23,12 @@ impl Cmd {
args: Vec::new(), args: Vec::new(),
stdin: None, stdin: None,
chdir: None, chdir: None,
env: HashMap::new(),
} }
} }
pub fn arg<T: AsRef<str>>(self, arg: T) -> Cmd { pub fn arg<T: AsRef<str>>(self, arg: T) -> Cmd {
let Self { program, mut args, stdin, chdir, .. } = self; let Self { program, mut args, stdin, chdir, env, .. } = self;
args.push(arg.as_ref().to_string()); args.push(arg.as_ref().to_string());
@@ -30,12 +37,13 @@ impl Cmd {
args, args,
stdin, stdin,
chdir, chdir,
env,
} }
} }
pub fn args<T: AsRef<str>>(self, new_args: & [ T ]) -> Cmd { pub fn args<T: AsRef<str>>(self, new_args: & [ T ]) -> Cmd {
let Self { program, mut args, stdin, chdir, .. } = self; let Self { program, mut args, stdin, chdir, env, .. } = self;
for arg in new_args { for arg in new_args {
args.push(arg.as_ref().to_string()); args.push(arg.as_ref().to_string());
@@ -46,10 +54,12 @@ impl Cmd {
args, args,
stdin, stdin,
chdir, chdir,
} } env,
}
}
pub fn stdin<T: AsRef<str>>(self, input: T) -> Cmd { pub fn stdin<T: AsRef<str>>(self, input: T) -> Cmd {
let Self { program, args, mut stdin, chdir, .. } = self; let Self { program, args, mut stdin, chdir, env, .. } = self;
stdin = if let Some(mut s) = stdin { stdin = if let Some(mut s) = stdin {
s.push_str(input.as_ref()); s.push_str(input.as_ref());
@@ -63,25 +73,47 @@ impl Cmd {
args, args,
stdin, stdin,
chdir, chdir,
env,
} }
} }
pub fn chdir<T: AsRef<str>>(self, dir: T) -> Cmd { pub fn chdir<T: AsRef<str>>(self, dir: T) -> Cmd {
let Self { program, args, stdin, .. } = self; let Self { program, args, stdin, env, .. } = self;
Cmd { Cmd {
program, program,
args, args,
stdin, stdin,
chdir: Some(PathBuf::from(dir.as_ref())), chdir: Some(PathBuf::from(dir.as_ref())),
env,
}
}
pub fn env<K: AsRef<str>, V: AsRef<str>>(self, key: K, value: V) -> Cmd {
let Self { program, args, stdin, chdir, mut env, .. } = self;
env.insert(key.as_ref().to_string(), value.as_ref().to_string());
Cmd {
program,
args,
stdin,
chdir,
env,
} }
} }
pub fn run(self) -> Result<CmdOutput, std::io::Error> { pub fn run(self) -> Result<CmdOutput, std::io::Error> {
let Self { program, args, stdin, chdir, .. } = self; let Self { program, args, stdin, chdir, env, .. } = self;
let mut cmd = duct_cmd(&program, &args); let mut cmd = duct_cmd(&program, &args);
if ! env.is_empty() {
for (key, value) in env {
cmd = cmd.env(key, value);
}
}
if let Some(stdin) = &stdin { if let Some(stdin) = &stdin {
cmd = cmd.stdin_bytes(stdin.as_bytes()); cmd = cmd.stdin_bytes(stdin.as_bytes());
} }
+1
View File
@@ -1 +1,2 @@
pub mod cmd; pub mod cmd;
pub mod serde;
+34
View File
@@ -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<Vec<String>, D::Error>
where D: Deserializer<'de>
{
struct StringOrVec(PhantomData<Vec<String>>);
impl<'de> Visitor<'de> for StringOrVec {
type Value = Vec<String>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or list of strings")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where E: serde::de::Error
{
Ok(vec![value.to_owned()])
}
fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
where S: SeqAccess<'de>
{
Deserialize::deserialize(SeqAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(StringOrVec(PhantomData))
}
Executable
+91
View File
@@ -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
+19
View File
@@ -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" ],
),
)
]
)
+7
View File
@@ -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$"