use serde::{Deserialize, Serialize};
use std::path::PathBuf;
pub const CRD_POLICY_ANNOTATION: &str = "sherpack.io/crd-policy";
pub const HELM_RESOURCE_POLICY: &str = "helm.sh/resource-policy";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum CrdPolicy {
#[default]
Managed,
Shared,
External,
}
impl CrdPolicy {
pub fn from_annotation(value: &str) -> Option<Self> {
match value.to_lowercase().as_str() {
"managed" => Some(Self::Managed),
"shared" => Some(Self::Shared),
"external" => Some(Self::External),
_ => None,
}
}
pub fn allows_install(&self) -> bool {
matches!(self, Self::Managed | Self::Shared)
}
pub fn allows_update(&self) -> bool {
matches!(self, Self::Managed | Self::Shared)
}
pub fn allows_delete(&self) -> bool {
matches!(self, Self::Managed)
}
pub fn description(&self) -> &'static str {
match self {
Self::Managed => "owned by this release",
Self::Shared => "shared between releases",
Self::External => "managed externally",
}
}
}
impl std::fmt::Display for CrdPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Managed => write!(f, "managed"),
Self::Shared => write!(f, "shared"),
Self::External => write!(f, "external"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CrdLocation {
CrdsDirectory {
path: PathBuf,
templated: bool,
},
Templates {
path: PathBuf,
},
Dependency {
dependency_name: String,
inner_location: Box<CrdLocation>,
},
}
impl CrdLocation {
pub fn crds_directory(path: impl Into<PathBuf>, templated: bool) -> Self {
Self::CrdsDirectory {
path: path.into(),
templated,
}
}
pub fn templates(path: impl Into<PathBuf>) -> Self {
Self::Templates { path: path.into() }
}
pub fn dependency(name: impl Into<String>, inner: CrdLocation) -> Self {
Self::Dependency {
dependency_name: name.into(),
inner_location: Box::new(inner),
}
}
pub fn is_from_crds_dir(&self) -> bool {
matches!(self, Self::CrdsDirectory { .. })
}
pub fn is_templated(&self) -> bool {
match self {
Self::CrdsDirectory { templated, .. } => *templated,
Self::Templates { .. } => true, Self::Dependency { inner_location, .. } => inner_location.is_templated(),
}
}
pub fn path(&self) -> &PathBuf {
match self {
Self::CrdsDirectory { path, .. } => path,
Self::Templates { path } => path,
Self::Dependency { inner_location, .. } => inner_location.path(),
}
}
pub fn description(&self) -> String {
match self {
Self::CrdsDirectory { path, templated } => {
if *templated {
format!("crds/{} (templated)", path.display())
} else {
format!("crds/{}", path.display())
}
}
Self::Templates { path } => format!("templates/{}", path.display()),
Self::Dependency {
dependency_name,
inner_location,
} => format!(
"dependency:{}/{}",
dependency_name,
inner_location.description()
),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrdOwnership {
pub crd_name: String,
pub owning_release: String,
pub release_namespace: String,
pub policy: CrdPolicy,
pub location: CrdLocation,
pub installed_version: Option<String>,
}
impl CrdOwnership {
pub fn new(
crd_name: impl Into<String>,
release: impl Into<String>,
namespace: impl Into<String>,
policy: CrdPolicy,
location: CrdLocation,
) -> Self {
Self {
crd_name: crd_name.into(),
owning_release: release.into(),
release_namespace: namespace.into(),
policy,
location,
installed_version: None,
}
}
pub fn with_version(mut self, version: impl Into<String>) -> Self {
self.installed_version = Some(version.into());
self
}
pub fn can_manage(&self, release: &str, namespace: &str) -> bool {
self.owning_release == release && self.release_namespace == namespace
}
}
#[derive(Debug, Clone)]
pub struct DetectedCrd {
pub name: String,
pub content: String,
pub location: CrdLocation,
pub policy: CrdPolicy,
pub has_keep_annotation: bool,
}
impl DetectedCrd {
pub fn new(name: impl Into<String>, content: impl Into<String>, location: CrdLocation) -> Self {
let content = content.into();
let (policy, has_keep_annotation) = Self::extract_policy(&content);
Self {
name: name.into(),
content,
location,
policy,
has_keep_annotation,
}
}
fn extract_policy(content: &str) -> (CrdPolicy, bool) {
let parsed: Result<serde_yaml::Value, _> = serde_yaml::from_str(content);
let Ok(value) = parsed else {
return (CrdPolicy::default(), false);
};
let annotations = value
.get("metadata")
.and_then(|m| m.get("annotations"))
.and_then(|a| a.as_mapping());
let Some(annotations) = annotations else {
return (CrdPolicy::default(), false);
};
let policy = annotations
.get(serde_yaml::Value::String(CRD_POLICY_ANNOTATION.to_string()))
.and_then(|v| v.as_str())
.and_then(CrdPolicy::from_annotation)
.unwrap_or_default();
let has_keep = annotations
.get(serde_yaml::Value::String(HELM_RESOURCE_POLICY.to_string()))
.and_then(|v| v.as_str())
.is_some_and(|v| v == "keep");
(policy, has_keep)
}
pub fn is_protected(&self) -> bool {
!self.policy.allows_delete()
|| self.has_keep_annotation
|| self.policy == CrdPolicy::Managed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crd_policy_default() {
assert_eq!(CrdPolicy::default(), CrdPolicy::Managed);
}
#[test]
fn test_crd_policy_from_annotation() {
assert_eq!(
CrdPolicy::from_annotation("managed"),
Some(CrdPolicy::Managed)
);
assert_eq!(
CrdPolicy::from_annotation("SHARED"),
Some(CrdPolicy::Shared)
);
assert_eq!(
CrdPolicy::from_annotation("External"),
Some(CrdPolicy::External)
);
assert_eq!(CrdPolicy::from_annotation("invalid"), None);
}
#[test]
fn test_crd_policy_permissions() {
let managed = CrdPolicy::Managed;
assert!(managed.allows_install());
assert!(managed.allows_update());
assert!(managed.allows_delete());
let shared = CrdPolicy::Shared;
assert!(shared.allows_install());
assert!(shared.allows_update());
assert!(!shared.allows_delete());
let external = CrdPolicy::External;
assert!(!external.allows_install());
assert!(!external.allows_update());
assert!(!external.allows_delete());
}
#[test]
fn test_crd_location_crds_dir() {
let loc = CrdLocation::crds_directory("mycrd.yaml", false);
assert!(loc.is_from_crds_dir());
assert!(!loc.is_templated());
assert_eq!(loc.description(), "crds/mycrd.yaml");
}
#[test]
fn test_crd_location_crds_dir_templated() {
let loc = CrdLocation::crds_directory("mycrd.yaml", true);
assert!(loc.is_from_crds_dir());
assert!(loc.is_templated());
assert_eq!(loc.description(), "crds/mycrd.yaml (templated)");
}
#[test]
fn test_crd_location_templates() {
let loc = CrdLocation::templates("operator-crd.yaml");
assert!(!loc.is_from_crds_dir());
assert!(loc.is_templated());
assert_eq!(loc.description(), "templates/operator-crd.yaml");
}
#[test]
fn test_crd_location_dependency() {
let inner = CrdLocation::crds_directory("cert.yaml", false);
let loc = CrdLocation::dependency("cert-manager", inner);
assert!(!loc.is_templated());
assert_eq!(loc.description(), "dependency:cert-manager/crds/cert.yaml");
}
#[test]
fn test_detected_crd_extracts_policy() {
let content = r#"
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tests.example.com
annotations:
sherpack.io/crd-policy: shared
"#;
let crd = DetectedCrd::new(
"tests.example.com",
content,
CrdLocation::crds_directory("test.yaml", false),
);
assert_eq!(crd.policy, CrdPolicy::Shared);
assert!(!crd.has_keep_annotation);
}
#[test]
fn test_detected_crd_extracts_keep_annotation() {
let content = r#"
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: tests.example.com
annotations:
helm.sh/resource-policy: keep
"#;
let crd = DetectedCrd::new(
"tests.example.com",
content,
CrdLocation::crds_directory("test.yaml", false),
);
assert_eq!(crd.policy, CrdPolicy::Managed); assert!(crd.has_keep_annotation);
assert!(crd.is_protected());
}
#[test]
fn test_crd_ownership() {
let ownership = CrdOwnership::new(
"tests.example.com",
"my-release",
"default",
CrdPolicy::Managed,
CrdLocation::crds_directory("test.yaml", false),
)
.with_version("1.0.0");
assert!(ownership.can_manage("my-release", "default"));
assert!(!ownership.can_manage("other-release", "default"));
assert!(!ownership.can_manage("my-release", "other-namespace"));
}
#[test]
fn test_crd_policy_serialization() {
assert_eq!(
serde_yaml::to_string(&CrdPolicy::Managed).unwrap().trim(),
"managed"
);
assert_eq!(
serde_yaml::to_string(&CrdPolicy::Shared).unwrap().trim(),
"shared"
);
assert_eq!(
serde_yaml::to_string(&CrdPolicy::External).unwrap().trim(),
"external"
);
}
}