use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
#[derive(DeriveTataraDomain, Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defencapsulates")]
pub struct EncapsulatesSpec {
pub kind: EncapsulationKind,
#[serde(default)]
pub mode: EncapsulationMode,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct EncapsulationKind {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub existing_helm_release: Option<ExistingHelmRelease>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub existing_kustomization: Option<ExistingKustomization>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bare_workload: Option<BareWorkload>,
}
#[derive(Clone, Debug)]
pub enum EncapsulationKindVariant<'a> {
ExistingHelmRelease(&'a ExistingHelmRelease),
ExistingKustomization(&'a ExistingKustomization),
BareWorkload(&'a BareWorkload),
}
impl EncapsulationKindVariant<'_> {
pub fn target(&self) -> EncapsulationTarget {
match self {
Self::ExistingHelmRelease(_) => EncapsulationTarget::ExistingHelmRelease,
Self::ExistingKustomization(_) => EncapsulationTarget::ExistingKustomization,
Self::BareWorkload(_) => EncapsulationTarget::BareWorkload,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, tatara_lisp::DeriveClosedSet)]
#[closed_set(via = "as_str", generate_unknown, display)]
pub enum EncapsulationTarget {
ExistingHelmRelease,
ExistingKustomization,
BareWorkload,
}
impl EncapsulationTarget {
pub const ALL: [Self; 3] = [
Self::ExistingHelmRelease,
Self::ExistingKustomization,
Self::BareWorkload,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::ExistingHelmRelease => "existingHelmRelease",
Self::ExistingKustomization => "existingKustomization",
Self::BareWorkload => "bareWorkload",
}
}
pub fn select<'a>(self, kind: &'a EncapsulationKind) -> Option<EncapsulationKindVariant<'a>> {
match self {
Self::ExistingHelmRelease => kind
.existing_helm_release
.as_ref()
.map(EncapsulationKindVariant::ExistingHelmRelease),
Self::ExistingKustomization => kind
.existing_kustomization
.as_ref()
.map(EncapsulationKindVariant::ExistingKustomization),
Self::BareWorkload => kind
.bare_workload
.as_ref()
.map(EncapsulationKindVariant::BareWorkload),
}
}
}
#[derive(Clone, Copy, Debug, thiserror::Error, PartialEq, Eq)]
pub enum EncapsulationKindError {
#[error("encapsulation kind has no variant set (one of {0} required)")]
Empty(&'static str),
#[error("encapsulation kind has multiple variants set; exactly one required")]
Ambiguous,
}
const ENCAPSULATION_TARGET_LIST: &str = "existingHelmRelease/existingKustomization/bareWorkload";
impl EncapsulationKind {
pub fn variant(&self) -> Result<EncapsulationKindVariant<'_>, EncapsulationKindError> {
use crate::tagged_union::{resolve, ResolveError};
resolve(EncapsulationTarget::ALL.into_iter().map(|t| t.select(self))).map_err(|e| match e {
ResolveError::None => EncapsulationKindError::Empty(ENCAPSULATION_TARGET_LIST),
ResolveError::Many => EncapsulationKindError::Ambiguous,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExistingHelmRelease {
pub namespace: String,
pub name: String,
pub release_name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExistingKustomization {
pub namespace: String,
pub name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct BareWorkload {
pub namespace: String,
pub selector: BTreeMap<String, String>,
}
#[derive(
Clone,
Copy,
Debug,
Default,
Serialize,
Deserialize,
JsonSchema,
PartialEq,
Eq,
Hash,
tatara_lisp::DeriveClosedSet,
)]
#[serde(rename_all = "PascalCase")]
#[closed_set(via = "as_str", generate_unknown, display)]
pub enum EncapsulationMode {
#[default]
Manage,
Adopt,
Observe,
}
impl EncapsulationMode {
pub const ALL: [Self; 3] = [Self::Manage, Self::Adopt, Self::Observe];
pub const fn as_str(self) -> &'static str {
match self {
Self::Manage => "Manage",
Self::Adopt => "Adopt",
Self::Observe => "Observe",
}
}
pub const fn emits_workload(self) -> bool {
match self {
Self::Manage | Self::Adopt => true,
Self::Observe => false,
}
}
pub const fn preserves_release_name(self) -> bool {
match self {
Self::Adopt => true,
Self::Manage | Self::Observe => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn akeyless_adopt() -> EncapsulatesSpec {
EncapsulatesSpec {
kind: EncapsulationKind {
existing_helm_release: Some(ExistingHelmRelease {
namespace: "akeyless".into(),
name: "akeyless-saas".into(),
release_name: "akeyless-saas-consolidated".into(),
}),
..EncapsulationKind::default()
},
mode: EncapsulationMode::Adopt,
}
}
#[test]
fn kind_empty_errors() {
let k = EncapsulationKind::default();
assert_eq!(
k.variant().unwrap_err(),
EncapsulationKindError::Empty(ENCAPSULATION_TARGET_LIST)
);
}
#[test]
fn kind_existing_hr_resolves() {
let s = akeyless_adopt();
match s.kind.variant().unwrap() {
EncapsulationKindVariant::ExistingHelmRelease(h) => {
assert_eq!(h.namespace, "akeyless");
assert_eq!(h.release_name, "akeyless-saas-consolidated");
}
other => panic!("expected ExistingHelmRelease, got {other:?}"),
}
}
#[test]
fn kind_two_variants_ambiguous() {
let k = EncapsulationKind {
existing_helm_release: Some(ExistingHelmRelease {
namespace: "ns".into(),
name: "n".into(),
release_name: "r".into(),
}),
existing_kustomization: Some(ExistingKustomization {
namespace: "ns".into(),
name: "n".into(),
}),
..EncapsulationKind::default()
};
assert_eq!(k.variant().unwrap_err(), EncapsulationKindError::Ambiguous);
}
#[test]
fn mode_dispatch() {
assert!(EncapsulationMode::Manage.emits_workload());
assert!(EncapsulationMode::Adopt.emits_workload());
assert!(!EncapsulationMode::Observe.emits_workload());
assert!(!EncapsulationMode::Manage.preserves_release_name());
assert!(EncapsulationMode::Adopt.preserves_release_name());
assert!(!EncapsulationMode::Observe.preserves_release_name());
}
#[test]
fn mode_default_is_manage() {
assert_eq!(EncapsulationMode::default(), EncapsulationMode::Manage);
}
#[test]
fn mode_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<EncapsulationMode>();
}
#[test]
fn mode_as_str_matches_serde() {
for mode in EncapsulationMode::ALL {
let serialized = serde_json::to_string(&mode).expect("serialize");
let unquoted = serialized
.trim_start_matches('"')
.trim_end_matches('"')
.to_string();
assert_eq!(
unquoted,
mode.as_str(),
"as_str drift for {mode:?}: as_str={} serde={unquoted}",
mode.as_str()
);
}
}
#[test]
fn mode_display_matches_as_str() {
for mode in EncapsulationMode::ALL {
assert_eq!(mode.to_string(), mode.as_str());
}
}
#[test]
fn unknown_encapsulation_mode_errors() {
use std::str::FromStr;
for bad in ["manage", "ADOPT", "Observed", "Wrap"] {
let err = EncapsulationMode::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn mode_projection_truth_table() {
let table: &[(EncapsulationMode, bool, bool)] = &[
(EncapsulationMode::Manage, true, false),
(EncapsulationMode::Adopt, true, true),
(EncapsulationMode::Observe, false, false),
];
assert_eq!(table.len(), EncapsulationMode::ALL.len());
for (mode, emits, preserves) in table {
assert_eq!(
mode.emits_workload(),
*emits,
"emits_workload drift for {mode:?}"
);
assert_eq!(
mode.preserves_release_name(),
*preserves,
"preserves_release_name drift for {mode:?}"
);
}
}
#[test]
fn mode_typed_projections_replace_raw_equality() {
for mode in EncapsulationMode::ALL {
assert_eq!(
!mode.emits_workload(),
mode == EncapsulationMode::Observe,
"!emits_workload() drift for {mode:?}"
);
assert_eq!(
mode.preserves_release_name(),
mode == EncapsulationMode::Adopt,
"preserves_release_name() drift for {mode:?}"
);
}
}
#[test]
fn serde_round_trip_via_yaml() {
let s = akeyless_adopt();
let yaml = serde_yaml::to_string(&s).unwrap();
assert!(yaml.contains("existingHelmRelease:"));
assert!(yaml.contains("releaseName: akeyless-saas-consolidated"));
assert!(yaml.contains("mode: Adopt"));
let back: EncapsulatesSpec = serde_yaml::from_str(&yaml).unwrap();
assert!(back.kind.existing_helm_release.is_some());
assert_eq!(back.mode, EncapsulationMode::Adopt);
}
#[test]
fn bare_workload_selector_round_trips() {
let mut sel = BTreeMap::new();
sel.insert("app".into(), "akeyless-gator".into());
sel.insert("tier".into(), "prod".into());
let s = EncapsulatesSpec {
kind: EncapsulationKind {
bare_workload: Some(BareWorkload {
namespace: "legacy".into(),
selector: sel,
}),
..EncapsulationKind::default()
},
mode: EncapsulationMode::Observe,
};
let yaml = serde_yaml::to_string(&s).unwrap();
assert!(yaml.contains("bareWorkload:"));
assert!(yaml.contains("app: akeyless-gator"));
assert!(yaml.contains("mode: Observe"));
let back: EncapsulatesSpec = serde_yaml::from_str(&yaml).unwrap();
match back.kind.variant().unwrap() {
EncapsulationKindVariant::BareWorkload(b) => {
assert_eq!(b.selector.len(), 2);
assert_eq!(
b.selector.get("app").map(String::as_str),
Some("akeyless-gator")
);
}
other => panic!("expected BareWorkload, got {other:?}"),
}
}
#[test]
fn lisp_round_trip_existing_hr() {
let src = r#"
(defencapsulates akeyless-adopt
:kind (:existing-helm-release
(:namespace "akeyless"
:name "akeyless-saas"
:release-name "akeyless-saas-consolidated"))
:mode Adopt)
"#;
let defs: Vec<tatara_lisp::NamedDefinition<EncapsulatesSpec>> =
tatara_lisp::compile_named::<EncapsulatesSpec>(src).expect("compile");
let d = &defs[0];
assert_eq!(d.name, "akeyless-adopt");
assert_eq!(d.spec.mode, EncapsulationMode::Adopt);
let h = d.spec.kind.existing_helm_release.as_ref().unwrap();
assert_eq!(h.namespace, "akeyless");
assert_eq!(h.release_name, "akeyless-saas-consolidated");
}
#[test]
fn lisp_default_mode_is_manage() {
let src = r#"
(defencapsulates greenfield
:kind (:existing-kustomization
(:namespace "flux-system"
:name "openclaw")))
"#;
let defs: Vec<tatara_lisp::NamedDefinition<EncapsulatesSpec>> =
tatara_lisp::compile_named::<EncapsulatesSpec>(src).expect("compile");
let d = &defs[0];
assert_eq!(d.spec.mode, EncapsulationMode::Manage);
}
fn single_slot_kind(target: EncapsulationTarget) -> EncapsulationKind {
match target {
EncapsulationTarget::ExistingHelmRelease => EncapsulationKind {
existing_helm_release: Some(ExistingHelmRelease {
namespace: "ns".into(),
name: "hr".into(),
release_name: "rel".into(),
}),
..EncapsulationKind::default()
},
EncapsulationTarget::ExistingKustomization => EncapsulationKind {
existing_kustomization: Some(ExistingKustomization {
namespace: "ns".into(),
name: "ks".into(),
}),
..EncapsulationKind::default()
},
EncapsulationTarget::BareWorkload => {
let mut sel = BTreeMap::new();
sel.insert("app".into(), "x".into());
EncapsulationKind {
bare_workload: Some(BareWorkload {
namespace: "ns".into(),
selector: sel,
}),
..EncapsulationKind::default()
}
}
}
}
fn two_slot_kind(a: EncapsulationTarget, b: EncapsulationTarget) -> EncapsulationKind {
let ka = single_slot_kind(a);
let kb = single_slot_kind(b);
EncapsulationKind {
existing_helm_release: ka.existing_helm_release.or(kb.existing_helm_release),
existing_kustomization: ka.existing_kustomization.or(kb.existing_kustomization),
bare_workload: ka.bare_workload.or(kb.bare_workload),
}
}
#[test]
fn encapsulation_target_is_well_formed_closed_set() {
tatara_lisp::assert_closed_set_well_formed::<EncapsulationTarget>();
}
#[test]
fn encapsulation_target_as_str_matches_field_name() {
for t in EncapsulationTarget::ALL {
let k = single_slot_kind(t);
let yaml = serde_yaml::to_string(&k).expect("serialize");
let key = t.as_str();
assert!(
yaml.contains(&format!("{key}:")),
"as_str(={key:?}) for {t:?} not present in serialized YAML:\n{yaml}"
);
}
}
#[test]
fn encapsulation_target_canonical_names_pinned() {
assert_eq!(
EncapsulationTarget::ExistingHelmRelease.as_str(),
"existingHelmRelease"
);
assert_eq!(
EncapsulationTarget::ExistingKustomization.as_str(),
"existingKustomization"
);
assert_eq!(EncapsulationTarget::BareWorkload.as_str(), "bareWorkload");
}
#[test]
fn encapsulation_target_display_matches_as_str() {
for t in EncapsulationTarget::ALL {
assert_eq!(t.to_string(), t.as_str());
}
}
#[test]
fn unknown_encapsulation_target_errors() {
use std::str::FromStr;
for bad in [
"ExistingHelmRelease",
"existing_helm_release",
"EXISTINGHELMRELEASE",
"helmRelease",
"kustomization",
"Manage",
"Adopt",
"Observe",
"OnAttested",
] {
let err = EncapsulationTarget::from_str(bad).unwrap_err();
assert_eq!(err.0, bad, "error payload should echo input verbatim");
}
}
#[test]
fn encapsulation_target_round_trips_through_variant_target() {
for t in EncapsulationTarget::ALL {
let k = single_slot_kind(t);
let v = t.select(&k).expect("populated slot must select");
assert_eq!(v.target(), t, "round-trip failed for {t:?}");
assert_eq!(
k.variant().expect("exactly-one variant").target(),
t,
"variant() resolver disagreed on {t:?}"
);
}
}
#[test]
fn encapsulation_target_select_returns_none_for_unset_slot() {
let empty = EncapsulationKind::default();
for t in EncapsulationTarget::ALL {
assert!(
t.select(&empty).is_none(),
"{t:?} reported populated on a default EncapsulationKind"
);
}
}
#[test]
fn encapsulation_kind_error_empty_lists_every_target_in_canonical_order() {
assert_eq!(
<EncapsulationTarget as tatara_lisp::ClosedSet>::labels_joined("/"),
ENCAPSULATION_TARGET_LIST,
);
let err = EncapsulationKind::default().variant().unwrap_err();
assert_eq!(
err,
EncapsulationKindError::Empty(ENCAPSULATION_TARGET_LIST)
);
}
#[test]
fn encapsulation_kind_two_slots_is_ambiguous_across_every_pair() {
for a in EncapsulationTarget::ALL {
for b in EncapsulationTarget::ALL {
if a == b {
continue;
}
let k = two_slot_kind(a, b);
assert_eq!(
k.variant().unwrap_err(),
EncapsulationKindError::Ambiguous,
"({a:?}, {b:?}) should resolve Ambiguous"
);
}
}
}
}