use crate::cli_types::ResourceMetadata;
use crate::config::DEFAULT_PROJECT_ID;
use crate::crd_types::CustomResource;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApplyResult {
Created,
Configured,
Unchanged,
}
pub const SYSTEM_PROJECT: &str = "_system";
pub fn is_project_scoped(kind: &str) -> bool {
matches!(
kind,
"Agent"
| "Workflow"
| "Workspace"
| "StepTemplate"
| "ExecutionProfile"
| "EnvStore"
| "SecretStore"
| "RuntimePolicy"
)
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResourceStore {
#[serde(default)]
resources: HashMap<String, CustomResource>,
#[serde(skip)]
generation: u64,
}
impl ResourceStore {
fn storage_key(kind: &str, metadata: &ResourceMetadata) -> String {
let project = metadata
.project
.as_deref()
.filter(|p| !p.trim().is_empty())
.unwrap_or(SYSTEM_PROJECT);
format!("{}/{}/{}", kind, project, metadata.name)
}
pub fn get(&self, kind: &str, name: &str) -> Option<&CustomResource> {
self.get_namespaced(kind, SYSTEM_PROJECT, name)
}
pub fn get_mut_by_key(&mut self, key: &str) -> Option<&mut CustomResource> {
self.resources.get_mut(key)
}
pub fn get_namespaced(&self, kind: &str, project: &str, name: &str) -> Option<&CustomResource> {
let key = format!("{}/{}/{}", kind, project, name);
self.resources.get(&key)
}
pub fn list_by_kind(&self, kind: &str) -> Vec<&CustomResource> {
let prefix = format!("{}/", kind);
self.resources
.iter()
.filter(|(k, _)| k.starts_with(&prefix))
.map(|(_, v)| v)
.collect()
}
pub fn list_by_kind_for_project(&self, kind: &str, project: &str) -> Vec<&CustomResource> {
let prefix = format!("{}/{}/", kind, project);
self.resources
.iter()
.filter(|(k, _)| k.starts_with(&prefix))
.map(|(_, v)| v)
.collect()
}
pub fn put(&mut self, mut cr: CustomResource) -> ApplyResult {
if is_project_scoped(&cr.kind)
&& cr
.metadata
.project
.as_deref()
.filter(|p| !p.trim().is_empty())
.is_none()
{
cr.metadata.project = Some(DEFAULT_PROJECT_ID.to_string());
}
let key = Self::storage_key(&cr.kind, &cr.metadata);
self.generation += 1;
match self.resources.get(&key) {
None => {
self.resources.insert(key, cr);
ApplyResult::Created
}
Some(existing) => {
if existing.spec == cr.spec
&& existing.api_version == cr.api_version
&& existing.metadata == cr.metadata
{
ApplyResult::Unchanged
} else {
self.resources.insert(key, cr);
ApplyResult::Configured
}
}
}
}
pub fn remove(&mut self, kind: &str, name: &str) -> Option<CustomResource> {
self.remove_namespaced(kind, SYSTEM_PROJECT, name)
}
pub fn remove_first_by_kind_name(&mut self, kind: &str, name: &str) -> Option<CustomResource> {
let suffix = format!("/{}", name);
let prefix = format!("{}/", kind);
let key = self
.resources
.keys()
.find(|k| k.starts_with(&prefix) && k.ends_with(&suffix) && k.matches('/').count() == 2)
.cloned();
if let Some(key) = key {
let removed = self.resources.remove(&key);
if removed.is_some() {
self.generation += 1;
}
return removed;
}
None
}
pub fn remove_namespaced(
&mut self,
kind: &str,
project: &str,
name: &str,
) -> Option<CustomResource> {
let key = format!("{}/{}/{}", kind, project, name);
let removed = self.resources.remove(&key);
if removed.is_some() {
self.generation += 1;
}
removed
}
pub fn remove_all_for_project(&mut self, project: &str) {
let pattern = format!("/{}/", project);
let before = self.resources.len();
self.resources.retain(|key, _| !key.contains(&pattern));
if self.resources.len() < before {
self.generation += 1;
}
}
pub fn generation(&self) -> u64 {
self.generation
}
pub fn is_empty(&self) -> bool {
self.resources.is_empty()
}
pub fn len(&self) -> usize {
self.resources.len()
}
pub fn resources(&self) -> &HashMap<String, CustomResource> {
&self.resources
}
pub fn resources_mut(&mut self) -> &mut HashMap<String, CustomResource> {
&mut self.resources
}
}