use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::flux_resource::FluxResource;
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Boundary {
#[serde(default)]
pub preconditions: Vec<Condition>,
#[serde(default)]
pub postconditions: Vec<Condition>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout: Option<String>,
}
impl Boundary {
#[must_use]
pub fn has_condition_kind(&self, kind: ConditionKind) -> bool {
self.preconditions.has_kind(kind) || self.postconditions.has_kind(kind)
}
}
pub trait ConditionSliceExt {
fn has_kind(&self, kind: ConditionKind) -> bool;
}
impl ConditionSliceExt for [Condition] {
fn has_kind(&self, kind: ConditionKind) -> bool {
self.iter().any(|c| c.kind == kind)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Condition {
pub kind: ConditionKind,
#[serde(default)]
#[schemars(schema_with = "crate::schema_helpers::preserve_unknown_object")]
pub params: serde_json::Value,
}
#[derive(
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
JsonSchema,
tatara_closed_set::DeriveClosedSet,
)]
#[serde(rename_all = "PascalCase")]
#[closed_set(via = "as_str", display, generate_unknown)]
pub enum ConditionKind {
ProcessPhase,
KustomizationHealthy,
HelmReleaseReleased,
PromQL,
Cel,
NixEval,
JobAttested,
ClosedLoopAuth,
}
impl ConditionKind {
pub const ALL: [Self; 8] = [
Self::ProcessPhase,
Self::KustomizationHealthy,
Self::HelmReleaseReleased,
Self::PromQL,
Self::Cel,
Self::NixEval,
Self::JobAttested,
Self::ClosedLoopAuth,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::ProcessPhase => "ProcessPhase",
Self::KustomizationHealthy => "KustomizationHealthy",
Self::HelmReleaseReleased => "HelmReleaseReleased",
Self::PromQL => "PromQL",
Self::Cel => "Cel",
Self::NixEval => "NixEval",
Self::JobAttested => "JobAttested",
Self::ClosedLoopAuth => "ClosedLoopAuth",
}
}
pub const fn stub_message(self) -> Option<&'static str> {
match self {
Self::PromQL => Some("PromQL evaluator not yet implemented"),
Self::Cel => Some("CEL evaluator not yet implemented"),
Self::NixEval => Some("NixEval evaluator not yet implemented"),
Self::ProcessPhase
| Self::KustomizationHealthy
| Self::HelmReleaseReleased
| Self::JobAttested
| Self::ClosedLoopAuth => None,
}
}
pub const fn is_stub(self) -> bool {
self.stub_message().is_some()
}
pub const fn flux_resource(self) -> Option<FluxResource> {
match self {
Self::KustomizationHealthy => Some(FluxResource::Kustomization),
Self::HelmReleaseReleased => Some(FluxResource::HelmRelease),
Self::ProcessPhase
| Self::PromQL
| Self::Cel
| Self::NixEval
| Self::JobAttested
| Self::ClosedLoopAuth => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn serde_process_phase_condition() {
let c = Condition {
kind: ConditionKind::ProcessPhase,
params: json!({ "processRef": "secret-injection", "phase": "Attested" }),
};
let yaml = serde_yaml::to_string(&c).unwrap();
assert!(yaml.contains("kind: ProcessPhase"));
assert!(yaml.contains("processRef: secret-injection"));
}
#[test]
fn serde_closed_loop_auth_condition() {
let c = Condition {
kind: ConditionKind::ClosedLoopAuth,
params: json!({
"issuer": { "service": "demo-app-issuer", "port": 8080 },
"consumer": { "service": "demo-app-gateway", "port": 8000 },
"probeImage": "ghcr.io/pleme-io/closed-loop-probe:0.1.0",
}),
};
let yaml = serde_yaml::to_string(&c).unwrap();
assert!(yaml.contains("kind: ClosedLoopAuth"));
assert!(yaml.contains("probeImage: ghcr.io/pleme-io/closed-loop-probe:0.1.0"));
let back: Condition = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(back.kind, ConditionKind::ClosedLoopAuth);
}
#[test]
fn serde_job_attested_condition() {
let c = Condition {
kind: ConditionKind::JobAttested,
params: json!({ "name": "seed-job", "namespace": "demo-test" }),
};
let yaml = serde_yaml::to_string(&c).unwrap();
assert!(yaml.contains("kind: JobAttested"));
}
#[test]
fn condition_kind_is_well_formed_closed_set() {
tatara_closed_set::assert_closed_set_well_formed::<ConditionKind>();
}
#[test]
fn condition_kind_as_str_matches_serde() {
crate::tagged_union::assert_label_matches_serde_serialization::<ConditionKind>();
}
#[test]
fn condition_kind_display_matches_as_str() {
crate::tagged_union::assert_display_matches_label::<ConditionKind>();
}
#[test]
fn unknown_condition_kind_errors() {
use std::str::FromStr;
for bad in ["processPhase", "PROMQL", "Promql", "Bogus"] {
let err = ConditionKind::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn condition_kind_stub_set_matches_stubs() {
use ConditionKind::*;
for kind in ConditionKind::ALL {
let expected_is_stub = matches!(kind, PromQL | Cel | NixEval);
assert_eq!(
kind.is_stub(),
expected_is_stub,
"is_stub disagreed for {kind:?}",
);
assert_eq!(
kind.stub_message().is_some(),
expected_is_stub,
"stub_message disagreed for {kind:?}",
);
}
}
#[test]
fn condition_kind_stub_messages_are_pinned() {
assert_eq!(
ConditionKind::PromQL.stub_message(),
Some("PromQL evaluator not yet implemented"),
);
assert_eq!(
ConditionKind::Cel.stub_message(),
Some("CEL evaluator not yet implemented"),
);
assert_eq!(
ConditionKind::NixEval.stub_message(),
Some("NixEval evaluator not yet implemented"),
);
}
#[test]
fn kustomization_healthy_projects_to_flux_resource_kustomization() {
assert_eq!(
ConditionKind::KustomizationHealthy.flux_resource(),
Some(FluxResource::Kustomization),
);
}
#[test]
fn helm_release_released_projects_to_flux_resource_helm_release() {
assert_eq!(
ConditionKind::HelmReleaseReleased.flux_resource(),
Some(FluxResource::HelmRelease),
);
}
#[test]
fn non_flux_fetching_kinds_project_to_none() {
use ConditionKind::*;
let non_flux: Vec<_> = ConditionKind::ALL
.iter()
.copied()
.filter(|k| k.flux_resource().is_none())
.collect();
assert_eq!(
non_flux,
vec![
ProcessPhase,
PromQL,
Cel,
NixEval,
JobAttested,
ClosedLoopAuth
],
);
}
#[test]
fn flux_resource_projection_is_injective_on_the_some_arms() {
let mut seen = std::collections::HashSet::new();
for k in ConditionKind::ALL {
if let Some(fr) = k.flux_resource() {
assert!(
seen.insert(fr),
"duplicate FluxResource projection at {k:?}: {fr:?}",
);
}
}
}
#[test]
fn flux_resource_projection_is_const_fn_reachable() {
const K: Option<FluxResource> = ConditionKind::KustomizationHealthy.flux_resource();
const H: Option<FluxResource> = ConditionKind::HelmReleaseReleased.flux_resource();
const P: Option<FluxResource> = ConditionKind::ProcessPhase.flux_resource();
assert_eq!(K, Some(FluxResource::Kustomization));
assert_eq!(H, Some(FluxResource::HelmRelease));
assert_eq!(P, None);
}
fn condition_with(kind: ConditionKind) -> Condition {
Condition {
kind,
params: json!({}),
}
}
#[test]
fn has_condition_kind_returns_false_on_empty_boundary_for_every_kind() {
let b = Boundary::default();
for kind in ConditionKind::ALL {
assert!(
!b.has_condition_kind(kind),
"default boundary must return false for {kind:?}",
);
}
}
#[test]
fn has_condition_kind_reads_postconditions_per_kind() {
for populated in ConditionKind::ALL {
let mut b = Boundary::default();
b.postconditions.push(condition_with(populated));
for query in ConditionKind::ALL {
let expected = query == populated;
assert_eq!(
b.has_condition_kind(query),
expected,
"postcondition populated={populated:?}: query {query:?} drifted",
);
}
}
}
#[test]
fn has_condition_kind_reads_preconditions_per_kind() {
for populated in ConditionKind::ALL {
let mut b = Boundary::default();
b.preconditions.push(condition_with(populated));
for query in ConditionKind::ALL {
let expected = query == populated;
assert_eq!(
b.has_condition_kind(query),
expected,
"precondition populated={populated:?}: query {query:?} drifted",
);
}
}
}
#[test]
fn has_condition_kind_unions_pre_and_post_condition_arms() {
let mut b = Boundary::default();
b.preconditions
.push(condition_with(ConditionKind::KustomizationHealthy));
b.postconditions
.push(condition_with(ConditionKind::ClosedLoopAuth));
assert!(
b.has_condition_kind(ConditionKind::KustomizationHealthy),
"pre-only kind must resolve through the union",
);
assert!(
b.has_condition_kind(ConditionKind::ClosedLoopAuth),
"post-only kind must resolve through the union",
);
assert!(
!b.has_condition_kind(ConditionKind::PromQL),
"an absent kind must return false even with populated halves",
);
}
#[test]
fn condition_slice_has_kind_returns_false_on_empty_slice_for_every_kind() {
let empty: &[Condition] = &[];
for kind in ConditionKind::ALL {
assert!(
!empty.has_kind(kind),
"empty slice must return false for {kind:?}",
);
}
}
#[test]
fn condition_slice_has_kind_reads_kind_field_per_variant() {
for populated in ConditionKind::ALL {
let slice = [condition_with(populated)];
for query in ConditionKind::ALL {
let expected = query == populated;
assert_eq!(
slice.has_kind(query),
expected,
"populated={populated:?}: query {query:?} drifted",
);
}
}
}
#[test]
fn condition_slice_has_kind_scans_beyond_the_first_position() {
let slice = [
condition_with(ConditionKind::KustomizationHealthy),
condition_with(ConditionKind::ClosedLoopAuth),
condition_with(ConditionKind::JobAttested),
];
for present in [
ConditionKind::KustomizationHealthy,
ConditionKind::ClosedLoopAuth,
ConditionKind::JobAttested,
] {
assert!(
slice.has_kind(present),
"kind at any position must resolve true: {present:?}",
);
}
for absent in [
ConditionKind::ProcessPhase,
ConditionKind::HelmReleaseReleased,
ConditionKind::PromQL,
ConditionKind::Cel,
ConditionKind::NixEval,
] {
assert!(
!slice.has_kind(absent),
"kind absent from the slice must resolve false: {absent:?}",
);
}
}
#[test]
fn boundary_has_condition_kind_equals_or_of_half_slice_probes() {
for pre_kind in ConditionKind::ALL {
for post_kind in ConditionKind::ALL {
let mut b = Boundary::default();
b.preconditions.push(condition_with(pre_kind));
b.postconditions.push(condition_with(post_kind));
for query in ConditionKind::ALL {
let expected =
b.preconditions.has_kind(query) || b.postconditions.has_kind(query);
assert_eq!(
b.has_condition_kind(query),
expected,
"union drifted: pre={pre_kind:?} post={post_kind:?} query={query:?}",
);
}
}
}
}
}