rci/
job.rs

1use crate::secret::Secret;
2use crate::task;
3use serde::{Deserialize, Serialize};
4
5fn default_timeout() -> u64 {
6    600
7}
8
9#[derive(bmart::tools::Sorting, Serialize, Deserialize, Default)]
10#[sorting(id = "name")]
11pub struct Info {
12    pub name: String,
13    pub last_task_id: Option<i64>,
14    pub last_task_created: Option<i64>,
15    pub last_task_status: Option<task::Status>,
16}
17
18#[derive(Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct Command {
21    kind: CommandKind,
22}
23
24impl Command {
25    #[allow(dead_code)]
26    pub fn new(kind: CommandKind) -> Self {
27        Self { kind }
28    }
29    pub fn kind(&self) -> CommandKind {
30        self.kind
31    }
32}
33
34#[derive(Serialize, Deserialize, Copy, Clone, Eq, PartialEq)]
35#[serde(rename_all = "lowercase")]
36pub enum CommandKind {
37    Run,
38    Terminate,
39    Kill,
40    Purge,
41}
42
43#[derive(Deserialize, Serialize, Eq, PartialEq)]
44#[serde(deny_unknown_fields)]
45pub struct Git {
46    pub url: Option<String>,
47    pub branch: String,
48}
49
50impl Default for Git {
51    fn default() -> Self {
52        Self {
53            url: None,
54            branch: "main".to_owned(),
55        }
56    }
57}
58
59#[derive(Deserialize, Serialize, Default, Eq, PartialEq)]
60#[serde(deny_unknown_fields)]
61pub struct Commands {
62    pub build: Option<String>,
63    pub test: Option<String>,
64    pub release: Option<String>,
65}
66
67#[derive(Deserialize, Serialize, Default, Eq, PartialEq)]
68#[serde(deny_unknown_fields)]
69pub struct OnCommands {
70    pub success: Option<String>,
71    pub fail: Option<String>,
72}
73
74#[derive(Deserialize, Serialize, Eq, PartialEq)]
75#[serde(deny_unknown_fields)]
76pub struct Job {
77    pub git: Git,
78    pub secret: Option<Secret>,
79    #[serde(default)]
80    pub commands: Commands,
81    #[serde(default)]
82    pub on: OnCommands,
83    #[serde(default = "default_timeout")]
84    pub timeout: u64,
85}
86
87impl Default for Job {
88    fn default() -> Self {
89        Job {
90            git: <_>::default(),
91            secret: Some(<_>::default()),
92            commands: <_>::default(),
93            on: <_>::default(),
94            timeout: default_timeout(),
95        }
96    }
97}
98
99#[cfg(feature = "ci")]
100pub mod ci {
101    use super::Info;
102    use crate::ci::db;
103    use crate::common::internal::{job_dir, work_dir};
104    use crate::error::Error;
105    use crate::task;
106    use std::future::Future;
107    use std::path::PathBuf;
108    use tokio::fs;
109    use tokio::io::AsyncWriteExt;
110
111    const ALLOWED_SYMBOLS: [char; 3] = ['_', '-', '.'];
112
113    pub async fn list() -> Result<Vec<String>, Error> {
114        let mut result = vec![];
115        let mut entries = fs::read_dir(job_dir()).await.map_err(Error::other)?;
116        while let Some(entry) = entries.next_entry().await.map_err(Error::other)? {
117            let path = entry.path();
118            if path.is_file() {
119                if let Some(ext) = path.extension() {
120                    if ext == "json" {
121                        if let Some(fname) = path.file_stem() {
122                            result.push(fname.to_string_lossy().to_string());
123                        }
124                    }
125                }
126            }
127        }
128        result.sort();
129        Ok(result)
130    }
131
132    #[inline]
133    pub fn list_tasks<'a>(
134        name: &'a str,
135        filter: &'a task::Filter,
136    ) -> impl Future<Output = Result<Vec<task::Info>, Error>> + 'a {
137        db::list_job_tasks(name, filter)
138    }
139
140    #[inline]
141    pub fn get_info(name: &str) -> impl Future<Output = Result<Info, Error>> + '_ {
142        db::get_job_info(name)
143    }
144
145    fn validate_name(name: &str) -> Result<(), Error> {
146        for c in name.chars() {
147            if !c.is_alphanumeric() && !ALLOWED_SYMBOLS.contains(&c) {
148                return Err(Error::other(format!("invalid character in job name: {c}")));
149            }
150        }
151        Ok(())
152    }
153
154    impl super::Job {
155        fn job_file(name: &str) -> PathBuf {
156            let mut job_file = job_dir().to_owned();
157            job_file.push(format!("{name}.json"));
158            job_file
159        }
160        pub fn exists(name: &str) -> bool {
161            Self::job_file(name).exists()
162        }
163        pub async fn purge(name: &str) -> Result<(), Error> {
164            if Self::exists(name) {
165                if !name.is_empty() {
166                    let mut work_dir = work_dir().to_owned();
167                    work_dir.push(name);
168                    if work_dir.exists() {
169                        fs::remove_dir_all(work_dir).await.map_err(Error::other)?;
170                    }
171                }
172                Ok(())
173            } else {
174                Err(Error::not_found("no such job"))
175            }
176        }
177        pub async fn load(name: &str) -> Result<Self, Error> {
178            validate_name(name)?;
179            let job_str = fs::read_to_string(Self::job_file(name)).await?;
180            let job = serde_json::from_str(&job_str)?;
181            Ok(job)
182        }
183        pub async fn save(&self, name: &str) -> Result<(), Error> {
184            validate_name(name)?;
185            let job_str = serde_json::to_string(&self).map_err(Error::ser)?;
186            fs::OpenOptions::new()
187                .create(true)
188                .truncate(true)
189                .write(true)
190                .append(false)
191                .open(Self::job_file(name))
192                .await
193                .map_err(Error::other)?
194                .write_all(job_str.as_bytes())
195                .await
196                .map_err(Error::other)?;
197            Ok(())
198        }
199        pub async fn delete(name: &str) -> Result<(), Error> {
200            validate_name(name)?;
201            fs::remove_file(Self::job_file(name)).await?;
202            let mut wdir = work_dir().to_owned();
203            wdir.push(name);
204            if wdir.exists() {
205                fs::remove_dir_all(wdir).await?;
206            }
207            Ok(())
208        }
209        pub fn secret_name(&self, s: &str) -> Option<&str> {
210            if let Some(ref secret) = self.secret {
211                secret.find_name(s)
212            } else {
213                None
214            }
215        }
216        pub fn trigger_secret(&self, trig: &str) -> Option<&str> {
217            if let Some(ref secret) = self.secret {
218                secret.find_trigger_secret(trig)
219            } else {
220                None
221            }
222        }
223    }
224}