diff --git a/examples/install_arch_packages.rh b/examples/install_arch_packages.rh new file mode 100644 index 00000000..ab53e3c0 --- /dev/null +++ b/examples/install_arch_packages.rh @@ -0,0 +1,11 @@ +#!/usr/bin/env rash + +- name: Install Rust dependencies + pacman: + executable: "{{ rash.dir }}/../rash_core/tests/mocks/pacman.rh" + extra_args: "--nodeps --nodeps" + force: true + name: + - rustup + - bpftrace + state: latest diff --git a/examples/task.rh b/examples/task.rh index 8cbf8420..234244a0 100755 --- a/examples/task.rh +++ b/examples/task.rh @@ -14,7 +14,7 @@ - debug: var: "find_result.extra" -- name: "save password to multiple files" +- name: save password to multiple files copy: content: "{{ env.MY_PASSWORD }}" dest: "/tmp/MY_PASSWORD_FILE_{{ file_name }}" diff --git a/rash_core/src/modules/file.rs b/rash_core/src/modules/file.rs index 1d7f599f..fdf8130f 100644 --- a/rash_core/src/modules/file.rs +++ b/rash_core/src/modules/file.rs @@ -20,7 +20,6 @@ /// /// - file: /// path: /yea -/// state: present /// mode: 0644 /// ``` /// ANCHOR_END: examples @@ -63,7 +62,7 @@ pub struct Params { /// If _file_, even with other options (such as mode), the file will be modified if it exists but /// will NOT be created if it does not exist. /// If _touch_, an empty file will be created if the file does not exist. - /// **[default: file]** + /// **[default: `"file"`]** state: Option, } diff --git a/rash_core/src/modules/find.rs b/rash_core/src/modules/find.rs index 0c1cf02f..d2792351 100644 --- a/rash_core/src/modules/find.rs +++ b/rash_core/src/modules/find.rs @@ -31,6 +31,7 @@ /// ANCHOR_END: examples use crate::error::{Error, ErrorKind, Result}; use crate::modules::{parse_if_json, parse_params, Module, ModuleResult}; +use crate::modules::utils::default_false; use crate::vars::Vars; #[cfg(feature = "docs")] @@ -47,15 +48,13 @@ use schemars::schema::RootSchema; use schemars::JsonSchema; use serde::Deserialize; use serde_with::{serde_as, OneOrMany}; -use serde_yaml::value; -use serde_yaml::Value; +use serde_yaml::{value, Value}; #[cfg(feature = "docs")] use strum_macros::{Display, EnumString}; -#[derive(Debug, PartialEq, Deserialize)] +#[derive(Default, Debug, PartialEq, Deserialize)] #[cfg_attr(feature = "docs", derive(EnumString, Display, JsonSchema))] #[serde(rename_all = "lowercase")] -#[derive(Default)] enum FileType { Any, Directory, @@ -68,10 +67,6 @@ fn default_file_type() -> Option { Some(FileType::default()) } -fn default_false() -> Option { - Some(false) -} - #[serde_as] #[derive(Debug, PartialEq, Deserialize)] #[cfg_attr(feature = "docs", derive(JsonSchema, DocJsonSchema))] @@ -85,15 +80,15 @@ pub struct Params { #[serde(default)] excludes: Option>, /// Type of file to select. - /// **[default: file]** + /// **[default: `"file"`]** #[serde(default = "default_file_type")] file_type: Option, /// Set this to true to follow symlinks - /// **[default: false]** + /// **[default: `false`]** #[serde(default = "default_false")] follow: Option, /// Set this to yes to include hidden files, otherwise they will be ignored. - /// **[default: false]** + /// **[default: `false`]** #[serde(default = "default_false")] hidden: Option, /// The patterns restrict the list of files to be returned to those whose basenames @@ -103,7 +98,7 @@ pub struct Params { #[serde(default)] patterns: Option>, /// If target is a directory, recursively descend into the directory looking for files. - /// **[default: false]** + /// **[default: `false`]** #[serde(default = "default_false")] recurse: Option, /// Select files whose size is less than the specified size. @@ -115,9 +110,8 @@ pub struct Params { impl Default for Params { fn default() -> Self { - let paths: Vec = Vec::new(); Params { - paths, + paths: Vec::new(), excludes: None, file_type: Some(FileType::default()), follow: Some(false), diff --git a/rash_core/src/modules/mod.rs b/rash_core/src/modules/mod.rs index c3099b20..bd2fc1b9 100644 --- a/rash_core/src/modules/mod.rs +++ b/rash_core/src/modules/mod.rs @@ -4,8 +4,10 @@ mod copy; mod debug; mod file; mod find; +mod pacman; mod set_vars; mod template; +mod utils; use crate::error::{Error, ErrorKind, Result}; use crate::modules::assert::Assert; @@ -14,6 +16,7 @@ use crate::modules::copy::Copy; use crate::modules::debug::Debug; use crate::modules::file::File; use crate::modules::find::Find; +use crate::modules::pacman::Pacman; use crate::modules::set_vars::SetVars; use crate::modules::template::Template; use crate::vars::Vars; @@ -85,6 +88,7 @@ lazy_static! { (Debug.get_name(), Box::new(Debug) as Box), (File.get_name(), Box::new(File) as Box), (Find.get_name(), Box::new(Find) as Box), + (Pacman.get_name(), Box::new(Pacman) as Box), (SetVars.get_name(), Box::new(SetVars) as Box), (Template.get_name(), Box::new(Template) as Box), ] diff --git a/rash_core/src/modules/pacman.rs b/rash_core/src/modules/pacman.rs new file mode 100644 index 00000000..ea671148 --- /dev/null +++ b/rash_core/src/modules/pacman.rs @@ -0,0 +1,206 @@ +/// ANCHOR: module +/// # pacman +/// +/// Manage packages with the pacman package manager, which is used by Arch Linux and its variants. +/// +/// ## Attributes +/// +/// ```yaml +/// check_mode: +/// support: always +/// ``` +/// ANCHOR_END: module +/// ANCHOR: examples +/// ## Example +/// +/// ```yaml +/// - name: Install package rustup from repo +/// pacman: +/// name: rustup +/// state: present +/// +/// - pacman: +/// executable: yay +/// name: +/// - rash +/// - timer-rs +/// state: present +/// ``` +/// ANCHOR_END: examples +use crate::error::{Error, ErrorKind, Result}; +use crate::modules::{parse_params, Module, ModuleResult}; +use crate::modules::utils::default_false; +use crate::vars::Vars; + +#[cfg(feature = "docs")] +use rash_derive::DocJsonSchema; + +#[cfg(feature = "docs")] +use schemars::schema::RootSchema; +#[cfg(feature = "docs")] +use schemars::JsonSchema; +use serde::Deserialize; +use serde_with::{serde_as, OneOrMany}; +use serde_yaml::Value; +#[cfg(feature = "docs")] +use strum_macros::{Display, EnumString}; + + +fn default_executable() -> Option { + Some("pacman".to_string()) +} + +#[derive(Default, Debug, PartialEq, Deserialize)] +#[cfg_attr(feature = "docs", derive(EnumString, Display, JsonSchema))] +#[serde(rename_all = "lowercase")] +enum State { + Absent, + Installed, + Latest, + #[default] + Present, + Removed, +} + +fn default_state() -> Option { + Some(State::default()) +} + +#[serde_as] +#[derive(Debug, PartialEq, Deserialize)] +#[cfg_attr(feature = "docs", derive(JsonSchema, DocJsonSchema))] +#[serde(deny_unknown_fields)] +pub struct Params { + /// Path of the binary to use. This can either be `pacman` or a pacman compatible AUR helper. + /// **[default: `"pacman"`]** + #[serde(default = "default_executable")] + executable: Option, + /// Additional option to pass to executable. + extra_args: Option, + /// When removing packages, forcefully remove them, without any checks. + /// Same as extra_args=”–nodeps –nodeps”. When combined with update_cache,] + /// force a refresh of all package databases. Same as update_cache_extra_args=”–refresh –refresh”. + /// **[default: `false`]** + #[serde(default = "default_false")] + force: Option, + /// Name or list of names of the package(s) or file(s) to install, upgrade, or remove. + #[serde_as(deserialize_as = "OneOrMany<_>")] + #[serde(default)] + name: Vec, + /// Whether to install (`present`, `latest`), or remove (`absent`) a package. + /// `present` will simply ensure that a desired package is installed. + /// `latest` will update the specified package if it is not of the latest available version. + /// `absent` will remove the specified package. + /// **[default: `"present"`]** + #[serde(default = "default_state")] + state: Option, +} + +impl Default for Params { + fn default() -> Self { + Params { + executable: Some("pacman".to_string()), + extra_args: None, + force: Some(false), + name: Vec::new(), + state: Some(State::Present), + } + } +} + + +#[derive(Debug)] +pub struct Pacman; + + +impl Module for Pacman { + fn get_name(&self) -> &str { + "pacman" + } + + fn exec(&self, optional_params: Value, vars: Vars, _check_mode: bool) -> Result<(ModuleResult, Vars)> { + Ok(( + pacman(parse_params(optional_params)?, &vars)?, + vars, + )) + } + + #[cfg(feature = "docs")] + fn get_json_schema(&self) -> Option { + Some(Params::get_json_schema()) + } +} + +fn pacman(params: Params, vars: &Vars) -> Result { + Ok(ModuleResult { + changed: false, + output: None, + extra: None, + }) +} + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_params() { + let yaml: Value = serde_yaml::from_str( + r#" + name: rustup + state: present + "#, + ) + .unwrap(); + let params: Params = parse_params(yaml).unwrap(); + assert_eq!( + params, + Params { + name: vec!["rustup".to_string()], + state: Some(State::Present), + ..Default::default() + } + ); + } + + #[test] + fn test_parse_params_all_values() { + let yaml: Value = serde_yaml::from_str( + r#" + executable: yay + extra_args: "--nodeps --nodeps" + force: true + name: + - rustup + - bpftrace + state: latest + "#, + ) + .unwrap(); + let params: Params = parse_params(yaml).unwrap(); + assert_eq!( + params, + Params { + executable: Some("yay".to_string()), + extra_args: Some("--nodeps --nodeps".to_string()), + force: Some(true), + name: vec!["rustup".to_string(), "bpftrace".to_string()], + state: Some(State::Latest), + } + ); + } + + #[test] + fn test_parse_params_random_field() { + let yaml: Value = serde_yaml::from_str( + r#" + name: rustup + foo: yea + "#, + ) + .unwrap(); + let error = parse_params::(yaml).unwrap_err(); + assert_eq!(error.kind(), ErrorKind::InvalidData); + } +} diff --git a/rash_core/src/modules/utils.rs b/rash_core/src/modules/utils.rs new file mode 100644 index 00000000..a951f6f8 --- /dev/null +++ b/rash_core/src/modules/utils.rs @@ -0,0 +1,9 @@ + +pub fn default_false() -> Option { + Some(false) +} + + +pub fn default_true() -> Option { + Some(true) +}