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 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 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 pub message: String,
225}
226
227#[derive(Clone, Debug, Serialize, Deserialize)]
228pub struct ActionError {
229 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}