1use crate::error::Result;
5use display_json::{DebugAsJson, DisplayAsJsonPretty};
6use envsubst;
7use schemars::{schema::RootSchema, schema_for, JsonSchema};
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, HashMap, HashSet};
10use std::ffi::OsStr;
11use std::path::{Path, PathBuf};
12use tokio::fs::read_to_string;
13
14pub type Env = HashMap<String, String>;
15
16pub trait EnvSubst {
17 fn subst_env(self, env: &Env) -> Self;
18}
19
20#[derive(
21 Default,
22 Clone,
23 Serialize,
24 Deserialize,
25 JsonSchema,
26 Eq,
27 PartialEq,
28 Hash,
29 DisplayAsJsonPretty,
30 DebugAsJson,
31)]
32#[serde(rename_all = "lowercase")]
33pub enum TestType {
34 Cluster,
35 #[default]
36 User,
37}
38
39#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
40pub struct TestSpec {
41 #[serde(default)]
42 pub name: String,
43 #[serde(default, rename = "type")]
44 pub test_type: TestType,
45 #[serde(default)]
46 pub ordering: Option<String>,
47 #[serde(default)]
48 pub steps: Vec<StepSpec>,
49 #[serde(skip_deserializing)]
50 pub dir: PathBuf,
51 #[serde(default)]
52 pub attempts: Option<u16>,
53}
54
55impl TestSpec {
56 pub async fn new_from_file(dirname: PathBuf) -> Result<TestSpec> {
57 let path = dirname.join(Path::new("test.yaml"));
58 let data = read_to_string(path).await?;
59 let mut testspec: TestSpec = serde_yaml::from_str(&data)?;
60 if testspec.name == "" {
61 let mut it = dirname.components();
62 let n2 = it.next_back().map_or_else(
63 || "".to_string(),
64 |x| {
65 let x: &OsStr = x.as_ref();
66 x.to_str().unwrap_or_default().to_string()
67 },
68 );
69 let n1 = it.next_back().map_or_else(
70 || "".to_string(),
71 |x| {
72 let x: &OsStr = x.as_ref();
73 x.to_str().unwrap_or_default().to_string()
74 },
75 );
76 testspec.name = format!("{n1}-{n2}");
77 }
78 testspec.dir = dirname;
79 Ok(testspec)
80 }
81
82 pub fn schema() -> RootSchema {
83 schema_for!(TestSpec)
84 }
85}
86
87#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
88pub struct StepSpec {
89 pub name: String,
90 #[serde(default)]
91 pub bucket: Vec<BucketSpec>,
92 #[serde(default)]
93 pub watch: Vec<WatchSpec>,
94 #[serde(default)]
95 pub apply: Vec<ApplySpec>,
96 #[serde(default)]
97 pub delete: Vec<ApplySpec>,
98 #[serde(default)]
99 pub script: Vec<ScriptSpec>,
100 #[serde(default)]
101 pub sleep: u16,
102 #[serde(default)]
103 pub wait: Vec<WaitSpec>,
104}
105
106pub type ScriptSpec = String;
107
108#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
109pub struct BucketSpec {
110 pub name: String,
111 pub operations: HashSet<BucketOperation>,
112}
113
114#[derive(
115 Clone, Serialize, Deserialize, JsonSchema, Eq, Hash, PartialEq, DisplayAsJsonPretty, DebugAsJson,
116)]
117#[serde(rename_all = "lowercase")]
118pub enum BucketOperation {
119 Create,
120 Patch,
121 Delete,
122}
123
124#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
125pub struct WatchSpec {
126 pub name: String,
127 #[serde(default)]
128 pub kind: String,
129 #[serde(default)]
130 pub group: String,
131 #[serde(default)]
132 pub version: String,
133 #[serde(default = "default_namespace")]
134 pub namespace: String,
135 #[serde(default)]
136 pub labels: Option<BTreeMap<String, String>>,
137 #[serde(default)]
138 pub fields: Option<BTreeMap<String, String>>,
139}
140
141impl EnvSubst for WatchSpec {
142 fn subst_env(self, env: &Env) -> Self {
143 WatchSpec {
144 name: self.name,
145 kind: subst_or_not(self.kind, env),
146 group: subst_or_not(self.group, env),
147 version: subst_or_not(self.version, env),
148 namespace: subst_or_not(self.namespace, env),
149 labels: self.labels,
150 fields: self.fields,
151 }
152 }
153}
154
155#[derive(Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
156pub struct ApplySpec {
157 pub path: String,
158 #[serde(default = "default_namespace")]
159 pub namespace: String,
160 #[serde(default = "default_override_namespace", rename = "override-namespace")]
161 pub override_namespace: bool,
162}
163
164fn default_override_namespace() -> bool {
165 true
166}
167fn default_namespace() -> String {
168 "${BLACKJACK_NAMESPACE}".to_string()
169}
170
171impl EnvSubst for ApplySpec {
172 fn subst_env(self, env: &Env) -> Self {
173 ApplySpec {
174 path: subst_or_not(self.path, env),
175 namespace: subst_or_not(self.namespace, env),
176 override_namespace: self.override_namespace,
177 }
178 }
179}
180
181#[derive(Clone, Serialize, Deserialize, JsonSchema, DisplayAsJsonPretty, DebugAsJson)]
182pub struct WaitSpec {
183 pub target: String,
184 pub condition: Expr,
185 pub timeout: u16,
186}
187
188impl EnvSubst for WaitSpec {
189 fn subst_env(self, env: &Env) -> Self {
190 WaitSpec {
191 target: self.target,
192 condition: self.condition.subst_env(env),
193 timeout: self.timeout,
194 }
195 }
196}
197
198#[derive(Clone, Serialize, Deserialize, JsonSchema, DebugAsJson)]
199#[serde(untagged)]
200pub enum Expr {
201 AndExpr { and: Vec<Expr> },
202 OrExpr { or: Vec<Expr> },
203 NotExpr { not: Box<Expr> },
204 SizeExpr { size: usize },
205 OneExpr { one: serde_json::Value },
206 AllExpr { all: serde_json::Value },
207}
208
209impl EnvSubst for Expr {
210 fn subst_env(self, env: &Env) -> Self {
211 match self {
212 Expr::AndExpr { and } => Expr::AndExpr {
213 and: and.into_iter().map(|expr| expr.subst_env(env)).collect(),
214 },
215 Expr::OrExpr { or } => Expr::OrExpr {
216 or: or.into_iter().map(|expr| expr.subst_env(env)).collect(),
217 },
218 Expr::NotExpr { not } => Expr::NotExpr {
219 not: Box::new(not.subst_env(env)),
220 },
221 Expr::SizeExpr { size } => Expr::SizeExpr { size },
222 Expr::OneExpr { one } => Expr::OneExpr {
223 one: env_subst_json(one, env),
224 },
225 Expr::AllExpr { all } => Expr::AllExpr {
226 all: env_subst_json(all, env),
227 },
228 }
229 }
230}
231
232impl std::fmt::Display for Expr {
233 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234 match self {
235 Expr::AndExpr { and } => {
236 let exprs: Vec<String> = and.iter().map(|e| format!("{}", e)).collect();
237 write!(f, "AND({})", exprs.join(", "))
238 }
239 Expr::OrExpr { or } => {
240 let exprs: Vec<String> = or.iter().map(|e| format!("{}", e)).collect();
241 write!(f, "OR({})", exprs.join(", "))
242 }
243 Expr::NotExpr { not } => {
244 write!(f, "NOT({})", not)
245 }
246 Expr::SizeExpr { size } => {
247 write!(f, "size == {}", size)
248 }
249 Expr::OneExpr { one } => {
250 write!(f, "ANY({})", one)
251 }
252 Expr::AllExpr { all } => {
253 write!(f, "ALL({})", all)
254 }
255 }
256 }
257}
258
259fn env_subst_json(value: serde_json::Value, env: &Env) -> serde_json::Value {
260 match value {
261 serde_json::Value::String(s) => serde_json::Value::String(subst_or_not(s, env)),
262 serde_json::Value::Array(arr) => {
263 let new_arr = arr.into_iter().map(|v| env_subst_json(v, env)).collect();
264 serde_json::Value::Array(new_arr)
265 }
266 serde_json::Value::Object(obj) => {
267 let new_obj = obj
268 .into_iter()
269 .map(|(k, v)| (k, env_subst_json(v, env)))
270 .collect();
271 serde_json::Value::Object(new_obj)
272 }
273 other => other,
274 }
275}
276
277fn subst_or_not(s: String, env: &Env) -> String {
278 envsubst::substitute(&s, env).or::<String>(Ok(s)).unwrap()
279}