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}