mod new;
mod valid;
use crate::error::{Error, ErrorKind, Result};
use crate::modules::{Module, ModuleResult};
use crate::task::new::TaskNew;
use crate::utils::tera::{is_render_string, render, render_as_json, render_string};
use crate::vars::Vars;
use rash_derive::FieldNames;
use std::process::exit;
use std::result::Result as StdResult;
use ipc_channel::ipc::IpcReceiver;
use ipc_channel::ipc::{self, IpcSender};
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{fork, setgid, setuid, ForkResult, Uid, User};
use serde_error::Error as SerdeError;
use serde_yaml::Value;
pub struct GlobalParams<'a> {
pub r#become: bool,
pub become_user: &'a str,
pub check_mode: bool,
}
impl Default for GlobalParams<'_> {
fn default() -> Self {
GlobalParams {
r#become: Default::default(),
become_user: "root",
check_mode: Default::default(),
}
}
}
#[derive(Debug, Clone, FieldNames)]
pub struct Task {
r#become: bool,
become_user: String,
check_mode: bool,
module: &'static dyn Module,
params: Value,
changed_when: Option<String>,
ignore_errors: Option<bool>,
name: Option<String>,
r#loop: Option<Value>,
register: Option<String>,
when: Option<String>,
}
pub type Tasks = Vec<Task>;
impl Task {
pub fn new(yaml: &Value, global_params: &GlobalParams) -> Result<Self> {
trace!("new task: {:?}", yaml);
TaskNew::from(yaml)
.validate_attrs()?
.get_task(global_params)
}
#[inline(always)]
fn is_attr(attr: &str) -> bool {
Self::get_field_names().contains(attr)
}
fn render_params(&self, vars: Vars) -> Result<Value> {
let original_params = self.params.clone();
match original_params {
Value::Mapping(map) => match map
.iter()
.filter_map(|t| match t.1 {
Value::String(s) => match render_string(s, &vars) {
Ok(s) => Some(Ok((t.0.clone(), Value::String(s)))),
Err(e) if e.kind() == ErrorKind::OmitParam => None,
Err(e) => Some(Err(e)),
},
Value::Sequence(x) => match x
.iter()
.map(|value| render(value.clone(), &vars))
.collect::<Result<Vec<Value>>>()
{
Ok(rendered_vec) => Some(Ok((t.0.clone(), Value::Sequence(rendered_vec)))),
Err(e) => Some(Err(e)),
},
_ => Some(Ok((t.0.clone(), t.1.clone()))),
})
.collect::<Result<_>>()
{
Ok(map) => Ok(Value::Mapping(map)),
Err(e) => Err(e),
},
Value::String(s) => Ok(Value::String(render_string(&s, &vars)?)),
_ => Err(Error::new(
ErrorKind::InvalidData,
format!("{original_params:?} must be a mapping or a string"),
)),
}
}
fn is_exec(&self, vars: &Vars) -> Result<bool> {
trace!("when: {:?}", &self.when);
match &self.when {
Some(s) => is_render_string(s, vars),
None => Ok(true),
}
}
fn get_iterator(value: &Value, vars: Vars) -> Result<Vec<Value>> {
match value.as_sequence() {
Some(v) => Ok(v
.iter()
.map(|item| render(item.clone(), &vars))
.collect::<Result<Vec<Value>>>()?),
None => Err(Error::new(ErrorKind::NotFound, "loop is not iterable")),
}
}
fn render_iterator(&self, vars: Vars) -> Result<Vec<Value>> {
let loop_some = self.r#loop.clone().unwrap();
match loop_some.as_str() {
Some(s) => {
let value: Value = serde_yaml::from_str(&render_as_json(s, &vars)?)?;
match value.as_str() {
Some(_) => Ok(vec![value]),
None => Task::get_iterator(&value, vars),
}
}
None => Task::get_iterator(&loop_some, vars),
}
}
fn is_changed(&self, result: &ModuleResult, vars: &Vars) -> Result<bool> {
trace!("changed_when: {:?}", &self.changed_when);
match &self.changed_when {
Some(s) => is_render_string(s, vars),
None => Ok(result.get_changed()),
}
}
fn exec_module_rendered_with_user(
&self,
rendered_params: &Value,
vars: &Vars,
user: User,
) -> Result<Vars> {
match setgid(user.gid) {
Ok(_) => match setuid(user.uid) {
Ok(_) => self.exec_module_rendered(rendered_params, vars),
Err(_) => Err(Error::new(
ErrorKind::Other,
format!("gid cannot be changed to {}", user.gid),
)),
},
Err(_) => Err(Error::new(
ErrorKind::Other,
format!("uid cannot be changed to {}", user.uid),
)),
}
}
fn exec_module_rendered(&self, rendered_params: &Value, vars: &Vars) -> Result<Vars> {
match self
.module
.exec(rendered_params.clone(), vars.clone(), self.check_mode)
{
Ok((result, result_vars)) => {
info!(target: if self.is_changed(&result, &result_vars)? {"changed"} else { "ok"},
"{}",
result.get_output().unwrap_or_else(
|| format!("{rendered_params:?}")
)
);
let mut new_vars = result_vars;
if self.register.is_some() {
let register = self.register.as_ref().unwrap();
trace!("register {:?} in {:?}", &result, register);
new_vars.insert(register, &result);
}
Ok(new_vars)
}
Err(e) => match self.ignore_errors {
Some(is_true) => {
if is_true {
info!(target: "ignoring", "{}", e);
Ok(vars.clone())
} else {
Err(e)
}
}
None => Err(e),
},
}
}
fn exec_module(&self, vars: Vars) -> Result<Vars> {
if self.is_exec(&vars)? {
let rendered_params = self.render_params(vars.clone())?;
match self.r#become {
true => {
let user = match User::from_name(&self.become_user)? {
Some(user) => Ok(user),
None => match self.become_user.parse::<u32>().map(Uid::from_raw) {
Ok(uid) => match User::from_uid(uid)? {
Some(user) => Ok(user),
None => Err(Error::new(
ErrorKind::NotFound,
format!("user: {} not found", &self.become_user),
)),
},
Err(e) => Err(Error::new(ErrorKind::Other, e)),
},
}?;
if user.uid != Uid::current() {
if self.module.get_name() == "command"
&& rendered_params["transfer_pid"].as_bool().unwrap_or(false)
{
return self.exec_module_rendered_with_user(
&rendered_params,
&vars,
user,
);
}
#[allow(clippy::type_complexity)]
let (tx, rx): (
IpcSender<StdResult<String, SerdeError>>,
IpcReceiver<StdResult<String, SerdeError>>,
) = ipc::channel().unwrap();
match unsafe { fork() } {
Ok(ForkResult::Child) => {
trace!("change uid to: {}", user.uid);
trace!("change gid to: {}", user.gid);
let result = self.exec_module_rendered_with_user(
&rendered_params,
&vars,
user,
);
trace!("send result: {:?}", result);
tx.send(
result
.map(|x| x.into_json().to_string())
.map_err(|e| SerdeError::new(&e)),
)
.unwrap_or_else(|e| {
error!("child failed to send result: {}", e);
exit(1)
});
exit(0);
}
Ok(ForkResult::Parent { child, .. }) => {
match waitpid(child, None) {
Ok(WaitStatus::Exited(_, 0)) => Ok(()),
Ok(WaitStatus::Exited(_, exit_code)) => Err(Error::new(
ErrorKind::SubprocessFail,
format!("child failed with exit_code {exit_code}"),
)),
Err(e) => Err(Error::new(ErrorKind::Other, e)),
_ => Err(Error::new(
ErrorKind::SubprocessFail,
format!("child {child} unknown status"),
)),
}?;
rx.recv()
.unwrap_or_else(|e| {
Err(SerdeError::new(&Error::new(
ErrorKind::Other,
format!("{e:?}"),
)))
})
.map(|x| {
tera::Context::from_value(serde_json::from_str(&x).unwrap())
.unwrap()
})
.map_err(|e| Error::new(ErrorKind::Other, e))
}
Err(e) => Err(Error::new(ErrorKind::Other, e)),
}
} else {
self.exec_module_rendered(&rendered_params, &vars)
}
}
false => self.exec_module_rendered(&rendered_params, &vars),
}
} else {
debug!("skipping");
Ok(vars)
}
}
pub fn exec(&self, vars: Vars) -> Result<Vars> {
debug!("Module: {}", self.module.get_name());
debug!("Params: {:?}", self.params);
if self.r#loop.is_some() {
let mut new_vars = vars.clone();
for item in self.render_iterator(vars)?.into_iter() {
new_vars.insert("item", &item);
new_vars = self.exec_module(new_vars.clone())?;
}
Ok(new_vars)
} else {
let new_vars = self.exec_module(vars)?;
Ok(new_vars)
}
}
pub fn get_name(&self) -> Option<String> {
self.name.clone()
}
pub fn get_rendered_name(&self, vars: Vars) -> Result<String> {
render_string(
&self
.name
.clone()
.ok_or_else(|| Error::new(ErrorKind::NotFound, "no name found"))?,
&vars,
)
}
pub fn get_module(&self) -> &dyn Module {
self.module
}
}
#[cfg(test)]
impl From<Value> for Task {
fn from(value: Value) -> Self {
TaskNew::from(&value)
.validate_attrs()
.unwrap()
.get_task(&GlobalParams::default())
.unwrap()
}
}
pub fn parse_file(tasks_file: &str, global_params: &GlobalParams) -> Result<Tasks> {
let tasks: Vec<Value> = serde_yaml::from_str(tasks_file)?;
tasks
.into_iter()
.map(|task| Task::new(&task, global_params))
.collect::<Result<Tasks>>()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vars;
use std::collections::HashMap;
use tera::Context;
#[test]
fn test_from_yaml() {
let s: String = r#"
name: 'Test task'
command: 'example'
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert_eq!(task.name.unwrap(), "Test task");
assert_eq!(&task.module.get_name(), &"command");
}
#[test]
fn test_from_yaml_no_module() {
let s = r#"
name: 'Test task'
no_module: 'example'
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task_err = Task::new(&yaml, &GlobalParams::default()).unwrap_err();
assert_eq!(task_err.kind(), ErrorKind::InvalidData);
}
#[test]
fn test_from_yaml_invalid_attr() {
let s = r#"
name: 'Test task'
command: 'example'
invalid_attr: 'foo'
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task_err = Task::new(&yaml, &GlobalParams::default()).unwrap_err();
assert_eq!(task_err.kind(), ErrorKind::InvalidData);
}
#[test]
fn test_is_exec() {
let s: String = r#"
when: "boo == 'test'"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(task.is_exec(&vars).unwrap());
}
#[test]
fn test_is_exec_parsed_bool() {
let s: String = r#"
when: "boo | bool"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "false")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task.is_exec(&vars).unwrap());
}
#[test]
fn test_is_exec_false() {
let s: String = r#"
when: "boo != 'test'"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task.is_exec(&vars).unwrap());
}
#[test]
fn test_is_exec_bool_false() {
let s: String = r#"
when: false
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task.is_exec(&vars).unwrap());
}
#[test]
fn test_is_exec_array() {
let s: String = r#"
when:
- true
- "boo == 'test'"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(task.is_exec(&vars).unwrap());
}
#[test]
fn test_is_exec_array_one_false() {
let s: String = r#"
when:
- false
- "boo == 'test'"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task.is_exec(&vars).unwrap());
}
#[test]
fn test_render_iterator() {
let s: String = r#"
command: 'example'
loop:
- 1
- 2
- 3
"#
.to_owned();
let vars = vars::from_iter(vec![].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert_eq!(
task.render_iterator(vars).unwrap(),
vec![Value::from(1), Value::from(2), Value::from(3)]
);
}
#[test]
fn test_is_changed() {
let s: String = r#"
changed_when: "boo == 'test'"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(task
.is_changed(&ModuleResult::new(false, None, None), &vars)
.unwrap(),);
}
#[test]
fn test_is_changed_bool_true() {
let s: String = r#"
changed_when: true
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(task
.is_changed(&ModuleResult::new(false, None, None), &vars)
.unwrap(),);
}
#[test]
fn test_is_changed_bool_false() {
let s: String = r#"
changed_when: false
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task
.is_changed(&ModuleResult::new(true, None, None), &vars)
.unwrap(),);
}
#[test]
fn test_is_changed_string_false() {
let s: String = r#"
changed_when: "false"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task
.is_changed(&ModuleResult::new(false, None, None), &vars)
.unwrap(),);
}
#[test]
fn test_is_changed_false() {
let s: String = r#"
changed_when: "boo != 'test'"
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task
.is_changed(&ModuleResult::new(false, None, None), &vars)
.unwrap(),);
}
#[test]
fn test_is_changed_array() {
let s: String = r#"
changed_when:
- "boo == 'test'"
- true
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(task
.is_changed(&ModuleResult::new(false, None, None), &vars)
.unwrap(),);
}
#[test]
fn test_is_changed_array_false() {
let s: String = r#"
changed_when:
- "boo == 'test'"
- false
command: 'example'
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert!(!task
.is_changed(&ModuleResult::new(true, None, None), &vars)
.unwrap(),);
}
#[test]
fn test_when_in_loop() {
let s: String = r#"
command: echo 'example'
loop:
- 1
- 2
- 3
when: item == 1
"#
.to_owned();
let vars = vars::from_iter(vec![].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
let result = task.exec(vars).unwrap();
let mut expected = Context::new();
expected.insert("item", &json!(3));
assert_eq!(result, expected);
}
#[test]
fn test_render_iterator_var() {
let s: String = r#"
command: 'example'
loop: "{{ range(end=3) }}"
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert_eq!(
task.render_iterator(vars).unwrap(),
vec![Value::from(0), Value::from(1), Value::from(2)]
);
}
#[test]
fn test_render_iterator_list_with_vars() {
let s: String = r#"
command: 'example'
loop:
- "{{ boo }}"
- 2
"#
.to_owned();
let vars = vars::from_iter(vec![("boo", "test")].into_iter());
let yaml: Value = serde_yaml::from_str(&s).unwrap();
let task = Task::from(yaml);
assert_eq!(
task.render_iterator(vars).unwrap(),
vec![Value::from("test"), Value::from(2)]
);
}
#[test]
fn test_task_execute() {
let s0 = r#"
name: task 1
command: echo foo
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s0).unwrap();
let task = Task::from(yaml);
let vars = vars::from_iter(vec![].into_iter());
let result = task.exec(vars.clone()).unwrap();
assert_eq!(result, vars);
}
#[test]
fn test_read_tasks() {
let file = r#"
#!/bin/rash
- name: task 1
command:
foo: boo
- name: task 2
command: boo
"#;
let tasks = parse_file(file, &GlobalParams::default()).unwrap();
assert_eq!(tasks.len(), 2);
let s0 = r#"
name: task 1
command:
foo: boo
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s0).unwrap();
let task_0 = Task::from(yaml);
assert_eq!(tasks[0].name, task_0.name);
assert_eq!(tasks[0].params, task_0.params);
assert_eq!(tasks[0].module.get_name(), task_0.module.get_name());
let s1 = r#"
name: task 2
command: boo
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s1).unwrap();
let task_1 = Task::from(yaml);
assert_eq!(tasks[1].name, task_1.name);
assert_eq!(tasks[1].params, task_1.params);
assert_eq!(tasks[1].module.get_name(), task_1.module.get_name());
}
#[test]
fn test_render_params() {
let s0 = r#"
name: task 1
command:
cmd: ls {{ directory }}
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s0).unwrap();
let task = Task::from(yaml);
let vars = Vars::from_serialize(
[("directory", "boo"), ("xuu", "zoo")]
.iter()
.cloned()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>(),
)
.unwrap();
let rendered_params = task.render_params(vars).unwrap();
assert_eq!(rendered_params["cmd"].as_str().unwrap(), "ls boo");
}
#[test]
fn test_render_params_no_hash_map() {
let s0 = r#"
name: task 1
command: ls {{ directory }}
"#
.to_owned();
let yaml: Value = serde_yaml::from_str(&s0).unwrap();
let task = Task::from(yaml);
let vars = Vars::from_serialize(
[("directory", "boo"), ("xuu", "zoo")]
.iter()
.cloned()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>(),
)
.unwrap();
let rendered_params = task.render_params(vars).unwrap();
assert_eq!(rendered_params.as_str().unwrap(), "ls boo");
}
}