1use chrono::{DateTime, Utc};
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8use crate::boundary::Condition;
9use crate::crd::Process;
10
11#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
13#[serde(rename_all = "camelCase")]
14pub struct ProcessCondition {
15 #[serde(rename = "type")]
16 pub type_: String,
17 pub status: String,
18 pub last_transition_time: DateTime<Utc>,
19 #[serde(default, skip_serializing_if = "Option::is_none")]
20 pub reason: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub message: Option<String>,
23}
24
25impl ProcessCondition {
26 pub fn ready(reason: impl Into<String>, message: Option<String>) -> Self {
27 Self {
28 type_: "Ready".into(),
29 status: "True".into(),
30 last_transition_time: Utc::now(),
31 reason: Some(reason.into()),
32 message,
33 }
34 }
35
36 pub fn not_ready(reason: impl Into<String>, message: impl Into<String>) -> Self {
37 Self {
38 type_: "Ready".into(),
39 status: "False".into(),
40 last_transition_time: Utc::now(),
41 reason: Some(reason.into()),
42 message: Some(message.into()),
43 }
44 }
45
46 pub fn attested(root: &str) -> Self {
47 Self {
48 type_: "Attested".into(),
49 status: "True".into(),
50 last_transition_time: Utc::now(),
51 reason: Some("AttestationWritten".into()),
52 message: Some(format!("composed_root={root}")),
53 }
54 }
55}
56
57#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
59#[serde(rename_all = "camelCase")]
60pub struct FluxResourceRef {
61 pub api_version: String,
62 pub kind: String,
63 pub name: String,
64 pub namespace: String,
65 #[serde(default)]
66 pub ready: bool,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub message: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub last_check: Option<DateTime<Utc>>,
71}
72
73#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct RenderedResourceCoords {
123 pub api_version: String,
126 pub kind: String,
128 pub name: String,
130 pub namespace: Option<String>,
139}
140
141impl RenderedResourceCoords {
142 pub fn from_json(res: &Value) -> anyhow::Result<Self> {
157 let api_version = res
158 .get("apiVersion")
159 .and_then(|v| v.as_str())
160 .ok_or_else(|| anyhow::anyhow!("rendered resource missing apiVersion"))?
161 .to_string();
162 let kind = res
163 .get("kind")
164 .and_then(|v| v.as_str())
165 .ok_or_else(|| anyhow::anyhow!("rendered resource missing kind"))?
166 .to_string();
167 let metadata = res.get("metadata");
168 let name = metadata
169 .and_then(|m| m.get("name"))
170 .and_then(|v| v.as_str())
171 .ok_or_else(|| anyhow::anyhow!("rendered resource missing metadata.name"))?
172 .to_string();
173 let namespace = metadata
174 .and_then(|m| m.get("namespace"))
175 .and_then(|v| v.as_str())
176 .map(str::to_string);
177 Ok(Self {
178 api_version,
179 kind,
180 name,
181 namespace,
182 })
183 }
184
185 pub fn namespace_or_default(&self) -> &str {
191 self.namespace
192 .as_deref()
193 .unwrap_or(Process::DEFAULT_NAMESPACE)
194 }
195}
196
197#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
199#[serde(rename_all = "camelCase")]
200pub struct CheckedCondition {
201 #[serde(flatten)]
202 pub condition: Condition,
203 pub satisfied: bool,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 pub last_check: Option<DateTime<Utc>>,
206 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub message: Option<String>,
208}
209
210#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
212#[serde(rename_all = "camelCase")]
213pub struct BoundaryStatus {
214 #[serde(default)]
215 pub preconditions: Vec<CheckedCondition>,
216 #[serde(default)]
217 pub postconditions: Vec<CheckedCondition>,
218 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub deadline: Option<DateTime<Utc>>,
221}
222
223#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
225#[serde(rename_all = "camelCase")]
226pub struct ComplianceStatus {
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub baseline: Option<String>,
229 pub satisfied: u32,
230 pub violated: u32,
231 pub total: u32,
232 #[serde(default)]
233 pub violations: Vec<String>,
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use serde_json::json;
240
241 #[test]
244 fn rendered_resource_coords_from_json_extracts_all_four_slots_when_present() {
245 let res = json!({
246 "apiVersion": "kustomize.toolkit.fluxcd.io/v1",
247 "kind": "Kustomization",
248 "metadata": {
249 "name": "observability-stack",
250 "namespace": "flux-system",
251 },
252 });
253 let c = RenderedResourceCoords::from_json(&res).expect("extract");
254 assert_eq!(c.api_version, "kustomize.toolkit.fluxcd.io/v1");
255 assert_eq!(c.kind, "Kustomization");
256 assert_eq!(c.name, "observability-stack");
257 assert_eq!(c.namespace.as_deref(), Some("flux-system"));
258 }
259
260 #[test]
261 fn rendered_resource_coords_from_json_captures_absent_namespace_as_none() {
262 let res = json!({
264 "apiVersion": "v1",
265 "kind": "Namespace",
266 "metadata": {"name": "demo-test"},
267 });
268 let c = RenderedResourceCoords::from_json(&res).expect("extract");
269 assert_eq!(c.namespace, None);
270 assert_eq!(c.name, "demo-test");
271 }
272
273 #[test]
274 fn rendered_resource_coords_from_json_errors_on_missing_api_version() {
275 let res = json!({"kind": "K", "metadata": {"name": "n"}});
276 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
277 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
278 }
279
280 #[test]
281 fn rendered_resource_coords_from_json_errors_on_missing_kind() {
282 let res = json!({"apiVersion": "v1", "metadata": {"name": "n"}});
283 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
284 assert_eq!(e.to_string(), "rendered resource missing kind");
285 }
286
287 #[test]
288 fn rendered_resource_coords_from_json_errors_on_missing_metadata_name() {
289 let res = json!({"apiVersion": "v1", "kind": "K", "metadata": {}});
290 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
291 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
292 }
293
294 #[test]
295 fn rendered_resource_coords_from_json_errors_on_missing_metadata_object() {
296 let res = json!({"apiVersion": "v1", "kind": "K"});
299 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
300 assert_eq!(e.to_string(), "rendered resource missing metadata.name");
301 }
302
303 #[test]
304 fn rendered_resource_coords_from_json_errors_on_non_string_slot() {
305 let res = json!({
309 "apiVersion": 42,
310 "kind": "K",
311 "metadata": {"name": "n"},
312 });
313 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
314 assert_eq!(e.to_string(), "rendered resource missing apiVersion");
315 }
316
317 #[test]
318 fn rendered_resource_coords_error_wording_is_canonical() {
319 let cases = [
325 (
326 "apiVersion",
327 json!({"kind": "K", "metadata": {"name": "n"}}),
328 ),
329 (
330 "kind",
331 json!({"apiVersion": "v1", "metadata": {"name": "n"}}),
332 ),
333 (
334 "metadata.name",
335 json!({"apiVersion": "v1", "kind": "K", "metadata": {}}),
336 ),
337 ];
338 for (slot, res) in cases {
339 let e = RenderedResourceCoords::from_json(&res).expect_err("must error");
340 assert_eq!(
341 e.to_string(),
342 format!("rendered resource missing {slot}"),
343 "slot {slot} error must be canonical"
344 );
345 }
346 }
347
348 #[test]
349 fn rendered_resource_coords_namespace_or_default_returns_slice_when_some() {
350 let c = RenderedResourceCoords {
351 api_version: "v1".into(),
352 kind: "K".into(),
353 name: "n".into(),
354 namespace: Some("prod".into()),
355 };
356 assert_eq!(c.namespace_or_default(), "prod");
357 }
358
359 #[test]
360 fn rendered_resource_coords_namespace_or_default_falls_back_when_none() {
361 let c = RenderedResourceCoords {
362 api_version: "v1".into(),
363 kind: "K".into(),
364 name: "n".into(),
365 namespace: None,
366 };
367 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
368 assert_eq!(c.namespace_or_default(), "default");
369 }
370
371 #[test]
372 fn rendered_resource_coords_namespace_fallback_shares_process_default_const() {
373 let c = RenderedResourceCoords {
381 api_version: "v1".into(),
382 kind: "K".into(),
383 name: "n".into(),
384 namespace: None,
385 };
386 assert_eq!(c.namespace_or_default(), Process::DEFAULT_NAMESPACE);
387 }
388}