comtrya_lib/actions/
mod.rs

1mod binary;
2mod command;
3mod directory;
4mod file;
5mod git;
6mod group;
7mod macos;
8mod package;
9mod user;
10
11use crate::contexts::Contexts;
12use crate::manifests::Manifest;
13use crate::steps::Step;
14use anyhow::anyhow;
15use binary::BinaryGitHub;
16use command::run::RunCommand;
17use directory::{DirectoryCopy, DirectoryCreate, DirectoryRemove};
18use file::chown::FileChown;
19use file::copy::FileCopy;
20use file::download::FileDownload;
21use file::link::FileLink;
22use file::remove::FileRemove;
23use file::unarchive::FileUnarchive;
24use git::GitClone;
25use group::add::GroupAdd;
26use macos::MacOSDefault;
27use package::{PackageInstall, PackageRepository};
28use rhai::Engine;
29use schemars::JsonSchema;
30use serde::{Deserialize, Serialize};
31use std::fmt::Display;
32use tracing::{error, warn};
33use user::add::UserAdd;
34
35use self::user::add_group::UserAddGroup;
36
37#[derive(JsonSchema, Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
38#[serde(deny_unknown_fields)]
39pub struct ConditionalVariantAction<T> {
40    #[serde(flatten)]
41    pub action: T,
42
43    #[serde(rename = "where")]
44    pub condition: Option<String>,
45
46    #[serde(default)]
47    pub variants: Vec<Variant<T>>,
48}
49
50#[derive(JsonSchema, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
51pub struct Variant<T> {
52    #[serde(flatten)]
53    pub action: T,
54
55    #[serde(rename = "where")]
56    pub condition: Option<String>,
57}
58
59impl<T> Action for ConditionalVariantAction<T>
60where
61    T: Action,
62{
63    fn summarize(&self) -> String {
64        self.action.summarize()
65    }
66
67    fn plan(&self, manifest: &Manifest, context: &Contexts) -> Result<Vec<Step>, anyhow::Error> {
68        let engine = Engine::new();
69        let mut scope = crate::contexts::to_rhai(context);
70
71        let variant = self.variants.iter().find(|variant| {
72            if variant.condition.is_none() {
73                return false;
74            }
75
76            // .unwrap() is safe here because we checked for None above
77            let condition = variant.condition.clone().unwrap();
78            match engine.eval_with_scope::<bool>(&mut scope, condition.as_str()) {
79                Ok(b) => b,
80                Err(error) => {
81                    error!("Failed execution condition for action: {}", error);
82                    false
83                }
84            }
85        });
86
87        if let Some(variant) = variant {
88            return variant.action.plan(manifest, context);
89        }
90
91        if self.condition.is_none() {
92            return self.action.plan(manifest, context);
93        }
94
95        // .unwrap() is safe here because we checked for None above
96        let condition = self.condition.as_ref().unwrap();
97
98        match engine.eval_with_scope::<bool>(&mut scope, condition.as_str()) {
99            Ok(true) => self.action.plan(manifest, context),
100            Ok(false) => Ok(vec![]),
101            Err(error) => Err(anyhow!("Failed execution condition for action: {}", error)),
102        }
103    }
104}
105
106#[derive(JsonSchema, Clone, Debug, Serialize, Deserialize)]
107#[serde(deny_unknown_fields, tag = "action")]
108pub enum Actions {
109    #[serde(rename = "command.run", alias = "cmd.run")]
110    CommandRun(ConditionalVariantAction<RunCommand>),
111
112    #[serde(rename = "directory.copy", alias = "dir.copy")]
113    DirectoryCopy(ConditionalVariantAction<DirectoryCopy>),
114
115    #[serde(rename = "directory.create", alias = "dir.create")]
116    DirectoryCreate(ConditionalVariantAction<DirectoryCreate>),
117
118    #[serde(rename = "file.copy")]
119    FileCopy(ConditionalVariantAction<FileCopy>),
120
121    #[serde(rename = "file.chown")]
122    FileChown(ConditionalVariantAction<FileChown>),
123
124    #[serde(rename = "file.download")]
125    FileDownload(ConditionalVariantAction<FileDownload>),
126
127    #[serde(rename = "file.link")]
128    FileLink(ConditionalVariantAction<FileLink>),
129
130    #[serde(rename = "file.remove")]
131    FileRemove(ConditionalVariantAction<FileRemove>),
132
133    #[serde(rename = "file.unarchive")]
134    FileUnarchive(ConditionalVariantAction<FileUnarchive>),
135
136    #[serde(rename = "directory.remove", alias = "dir.remove")]
137    DirectoryRemove(ConditionalVariantAction<DirectoryRemove>),
138
139    #[serde(
140        rename = "binary.github",
141        alias = "binary.gh",
142        alias = "bin.github",
143        alias = "bin.gh"
144    )]
145    BinaryGitHub(ConditionalVariantAction<BinaryGitHub>),
146
147    #[serde(rename = "git.clone")]
148    GitClone(ConditionalVariantAction<GitClone>),
149
150    #[serde(rename = "group.add")]
151    GroupAdd(ConditionalVariantAction<GroupAdd>),
152
153    #[serde(rename = "macos.default")]
154    MacOSDefault(ConditionalVariantAction<MacOSDefault>),
155
156    #[serde(rename = "package.install", alias = "package.installed")]
157    PackageInstall(ConditionalVariantAction<PackageInstall>),
158
159    #[serde(rename = "package.repository", alias = "package.repo")]
160    PackageRepository(ConditionalVariantAction<PackageRepository>),
161
162    #[serde(rename = "user.add")]
163    UserAdd(ConditionalVariantAction<UserAdd>),
164
165    #[serde(rename = "user.group")]
166    UserAddGroup(ConditionalVariantAction<UserAddGroup>),
167}
168
169impl Actions {
170    pub fn inner_ref(&self) -> &dyn Action {
171        match self {
172            Actions::BinaryGitHub(a) => a,
173            Actions::CommandRun(a) => a,
174            Actions::DirectoryCopy(a) => a,
175            Actions::DirectoryCreate(a) => a,
176            Actions::FileCopy(a) => a,
177            Actions::FileChown(a) => a,
178            Actions::FileDownload(a) => a,
179            Actions::FileLink(a) => a,
180            Actions::FileUnarchive(a) => a,
181            Actions::GitClone(a) => a,
182            Actions::GroupAdd(a) => a,
183            Actions::MacOSDefault(a) => a,
184            Actions::PackageInstall(a) => a,
185            Actions::PackageRepository(a) => a,
186            Actions::UserAdd(a) => a,
187            Actions::UserAddGroup(a) => a,
188            Actions::FileRemove(a) => a,
189            Actions::DirectoryRemove(a) => a,
190        }
191    }
192}
193
194impl Display for Actions {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        let name = match self {
197            Actions::CommandRun(_) => "command.run",
198            Actions::DirectoryCopy(_) => "directory.copy",
199            Actions::DirectoryCreate(_) => "directory.create",
200            Actions::FileCopy(_) => "file.copy",
201            Actions::FileChown(_) => "file.chown",
202            Actions::FileDownload(_) => "file.download",
203            Actions::FileLink(_) => "file.link",
204            Actions::FileRemove(_) => "file.remove",
205            Actions::FileUnarchive(_) => "file.unarchive",
206            Actions::DirectoryRemove(_) => "directory.remove",
207            Actions::BinaryGitHub(_) => "github.binary",
208            Actions::GitClone(_) => "git.clone",
209            Actions::GroupAdd(_) => "group.add",
210            Actions::MacOSDefault(_) => "macos.default",
211            Actions::PackageInstall(_) => "package.install",
212            Actions::PackageRepository(_) => "package.repository",
213            Actions::UserAdd(_) => "user.add",
214            Actions::UserAddGroup(_) => "user.group",
215        };
216
217        write!(f, "{}", name)
218    }
219}
220
221#[derive(Clone, Debug, Serialize, Deserialize)]
222pub struct ActionResult {
223    /// Output / response
224    pub message: String,
225}
226
227#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct ActionError {
229    /// Error message
230    pub message: String,
231}
232
233impl<E: std::error::Error> From<E> for ActionError {
234    fn from(e: E) -> Self {
235        ActionError {
236            message: format!("{}", e),
237        }
238    }
239}
240
241pub trait Action {
242    fn summarize(&self) -> String {
243        warn!("need to define action summarize");
244        "not found action summarize".to_string()
245    }
246    fn plan(&self, manifest: &Manifest, context: &Contexts) -> anyhow::Result<Vec<Step>>;
247}
248
249#[cfg(test)]
250mod tests {
251    use crate::actions::{command::run::RunCommand, Actions};
252    use crate::manifests::Manifest;
253
254    #[test]
255    fn can_parse_some_advanced_stuff() {
256        let content = r#"
257actions:
258- action: command.run
259  command: echo
260  args:
261    - hi
262  variants:
263    - where: Debian
264      command: halt
265"#;
266        let m: Manifest = serde_yml::from_str(content).unwrap();
267
268        let action = &m.actions[0];
269
270        let ext = match action {
271            Actions::CommandRun(cr) => cr,
272            _ => panic!("did not get a command to run"),
273        };
274
275        assert_eq!(
276            ext.action,
277            RunCommand {
278                command: "echo".into(),
279                args: vec!["hi".into()],
280                privileged: false,
281                dir: std::env::current_dir()
282                    .unwrap()
283                    .into_os_string()
284                    .into_string()
285                    .unwrap(),
286                ..Default::default()
287            }
288        );
289
290        let variant = &ext.variants[0];
291        assert_eq!(variant.condition, Some(String::from("Debian")));
292        assert_eq!(variant.action.command, "halt");
293    }
294}