use kube::CustomResource;
use schemars::{gen::SchemaGenerator, schema::Schema, JsonSchema};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(CustomResource, Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[kube(
group = "cloudnative.amberjs.io",
version = "v1",
kind = "AmberWorkload",
plural = "amberjsworkloads",
shortname = "bjw",
namespaced
)]
#[cfg_attr(test, derive(Default))]
pub struct AmberWorkloadSpec {
pub cluster_ref: String,
pub script_path: String,
pub script_args: Vec<String>,
pub environment: HashMap<String, String>,
pub replicas: u32,
pub resources: super::amberjs_cluster::ResourceRequirements,
pub hpa_config: HPAConfig,
pub execution: Option<ExecutionConfig>,
pub io_config: Option<IOConfig>,
pub persistence: Option<PersistenceConfig>,
pub networking: Option<NetworkingConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct HPAConfig {
pub enabled: bool,
pub min_replicas: u32,
pub max_replicas: u32,
pub target_cpu_percent: f64,
pub target_memory_percent: f64,
pub custom_metrics: Option<Vec<CustomMetric>>,
pub stabilization_window_seconds: Option<u64>,
pub scale_policy: Option<ScalePolicy>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct CustomMetric {
pub name: String,
pub metric_type: String,
pub target_value: String,
pub metric_selector: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct ScalePolicy {
pub policy_type: String,
pub value: u32,
pub period_seconds: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct ExecutionConfig {
pub mode: ExecutionMode,
pub timeout_seconds: Option<u64>,
pub retry: Option<RetryConfig>,
pub concurrency_limit: Option<u32>,
pub memory_limit_mb: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, Default)]
#[cfg_attr(test, derive(Default))]
pub enum ExecutionMode {
#[default]
Standalone = 0,
Distributed,
Batch,
Streaming,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct RetryConfig {
pub max_attempts: u32,
pub initial_backoff_seconds: u64,
pub backoff_multiplier: f64,
pub max_backoff_seconds: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct IOConfig {
pub inputs: Option<Vec<InputSource>>,
pub outputs: Option<Vec<OutputDestination>>,
pub buffer: Option<BufferConfig>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct InputSource {
pub source_type: String,
pub config: HashMap<String, String>,
pub format: Option<String>,
pub schema: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct OutputDestination {
pub dest_type: String,
pub config: HashMap<String, String>,
pub format: Option<String>,
pub compression: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct BufferConfig {
pub size_bytes: Option<u64>,
pub flush_interval_seconds: Option<u64>,
pub max_items: Option<u32>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct PersistenceConfig {
pub enabled: bool,
pub storage_class: Option<String>,
pub access_mode: Option<String>,
pub size: Option<String>,
pub mount_path: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct NetworkingConfig {
pub service: Option<ServiceConfig>,
pub ingress: Option<IngressConfig>,
pub network_policies: Option<Vec<NetworkPolicyConfig>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct ServiceConfig {
pub enabled: bool,
pub service_type: String,
pub ports: Option<Vec<ServicePort>>,
pub session_affinity: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct ServicePort {
pub name: Option<String>,
pub port: u32,
pub target_port: Option<u32>,
pub protocol: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct IngressConfig {
pub enabled: bool,
pub ingress_class: Option<String>,
pub annotations: Option<HashMap<String, String>>,
pub hosts: Option<Vec<IngressHost>>,
pub tls: Option<Vec<IngressTLS>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct IngressHost {
pub host: String,
pub paths: Vec<IngressPath>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct IngressPath {
pub path: String,
pub path_type: String,
pub backend: IngressBackend,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct IngressBackend {
pub service_name: String,
pub service_port: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct IngressTLS {
pub hosts: Option<Vec<String>>,
pub secret_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct NetworkPolicyConfig {
pub name: String,
pub policy_types: Vec<String>,
pub ingress: Option<Vec<NetworkPolicyRule>>,
pub egress: Option<Vec<NetworkPolicyRule>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct NetworkPolicyRule {
pub from: Option<Vec<NetworkPolicyPeer>>,
pub to: Option<Vec<NetworkPolicyPeer>>,
pub ports: Option<Vec<NetworkPolicyPort>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct NetworkPolicyPeer {
pub namespace_selector: Option<super::amberjs_cluster::LabelSelector>,
pub pod_selector: Option<super::amberjs_cluster::LabelSelector>,
pub ip_block: Option<NetworkPolicyIPBlock>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct NetworkPolicyIPBlock {
pub cidr: String,
pub except: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[cfg_attr(test, derive(Default))]
pub struct NetworkPolicyPort {
pub port: Option<u32>,
pub protocol: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_amberjs_workload_crd_creation() {
let workload: _ = AmberWorkload::new(
"my-workload",
AmberWorkloadSpec {
cluster_ref: "my-cluster".to_string(),
script_path: "/app/main.js".to_string(),
script_args: vec!["--mode=production".to_string()],
environment: HashMap::from([
("NODE_ENV".to_string(), "production".to_string()),
("AMBER_MODE".to_string(), "distributed".to_string()),
]),
replicas: 5,
resources: super::super::amberjs_cluster::ResourceRequirements {
cpu: "1".to_string(),
memory: "2Gi".to_string(),
disk: "10Gi".to_string(),
gpu: None,
},
hpa_config: HPAConfig {
enabled: true,
min_replicas: 2,
max_replicas: 20,
target_cpu_percent: 70.0,
target_memory_percent: 80.0,
custom_metrics: None,
stabilization_window_seconds: None,
scale_policy: None,
},
execution: None,
io_config: None,
persistence: None,
networking: None,
},
);
assert_eq!(workload.spec.cluster_ref, "my-cluster");
assert_eq!(workload.spec.script_path, "/app/main.js");
assert_eq!(workload.spec.replicas, 5);
assert!(workload.spec.hpa_config.enabled);
assert_eq!(workload.spec.hpa_config.min_replicas, 2);
assert_eq!(workload.spec.hpa_config.max_replicas, 20);
}
#[test]
fn test_hpa_config() {
let hpa: _ = HPAConfig {
enabled: true,
min_replicas: 3,
max_replicas: 50,
target_cpu_percent: 60.0,
target_memory_percent: 75.0,
custom_metrics: Some(vec![CustomMetric {
name: "requests_per_second".to_string(),
metric_type: "Pod".to_string(),
target_value: "100".to_string(),
metric_selector: None,
}]),
stabilization_window_seconds: Some(300),
scale_policy: Some(ScalePolicy {
policy_type: "Percent".to_string(),
value: 10,
period_seconds: 60,
}),
};
assert!(hpa.enabled);
assert_eq!(hpa.min_replicas, 3);
assert_eq!(hpa.max_replicas, 50);
assert_eq!(hpa.target_cpu_percent, 60.0);
assert!(hpa.custom_metrics.is_some());
assert_eq!(hpa.custom_metrics.as_ref().unwrap().len(), 1);
}
#[test]
fn test_execution_config() {
let execution: _ = ExecutionConfig {
mode: ExecutionMode::Distributed,
timeout_seconds: Some(300),
retry: Some(RetryConfig {
max_attempts: 3,
initial_backoff_seconds: 5,
backoff_multiplier: 2.0,
max_backoff_seconds: 60,
}),
concurrency_limit: Some(100),
memory_limit_mb: Some(512),
};
matches!(execution.mode, ExecutionMode::Distributed);
assert_eq!(execution.timeout_seconds, Some(300));
assert_eq!(execution.retry.as_ref().unwrap().max_attempts, 3);
assert_eq!(execution.concurrency_limit, Some(100));
}
#[test]
fn test_io_config() {
let io_config: _ = IOConfig {
inputs: Some(vec![InputSource {
source_type: "kafka".to_string(),
config: HashMap::from([
("brokers".to_string(), "kafka:9092".to_string()),
("topic".to_string(), "input-events".to_string()),
]),
format: Some("json".to_string()),
schema: None,
}]),
outputs: Some(vec![OutputDestination {
dest_type: "s3".to_string(),
config: HashMap::from([
("bucket".to_string(), "output-bucket".to_string()),
("region".to_string(), "us-west-2".to_string()),
]),
format: Some("parquet".to_string()),
compression: Some("gzip".to_string()),
}]),
buffer: Some(BufferConfig {
size_bytes: Some(1024 * 1024), flush_interval_seconds: Some(10),
max_items: Some(1000),
}),
};
assert_eq!(io_config.inputs.as_ref().unwrap().len(), 1);
assert_eq!(io_config.outputs.as_ref().unwrap().len(), 1);
assert_eq!(io_config.inputs.as_ref().unwrap()[0].source_type, "kafka");
assert_eq!(io_config.outputs.as_ref().unwrap()[0].dest_type, "s3");
assert_eq!(
io_config.buffer.as_ref().unwrap().size_bytes,
Some(1024 * 1024)
);
}
#[test]
fn test_networking_config() {
let networking: _ = NetworkingConfig {
service: Some(ServiceConfig {
enabled: true,
service_type: "ClusterIP".to_string(),
ports: Some(vec![ServicePort {
name: Some("http".to_string()),
port: 8080,
target_port: Some(8080),
protocol: Some("TCP".to_string()),
}]),
session_affinity: Some("ClientIP".to_string()),
}),
ingress: Some(IngressConfig {
enabled: true,
ingress_class: Some("nginx".to_string()),
annotations: Some(HashMap::from([(
"cert-manager.io/cluster-issuer".to_string(),
"letsencrypt".to_string(),
)])),
hosts: Some(vec![IngressHost {
host: "api.example.com".to_string(),
paths: vec![IngressPath {
path: "/".to_string(),
path_type: "Prefix".to_string(),
backend: IngressBackend {
service_name: "my-workload".to_string(),
service_port: 8080,
},
}],
}]),
tls: Some(vec![IngressTLS {
hosts: Some(vec!["api.example.com".to_string()]),
secret_name: Some("api-tls".to_string()),
}]),
}),
network_policies: None,
};
assert!(networking.service.as_ref().unwrap().enabled);
assert_eq!(
networking.service.as_ref().unwrap().service_type,
"ClusterIP"
);
assert!(networking.ingress.as_ref().unwrap().enabled);
assert_eq!(
networking
.ingress
.as_ref()
.unwrap()
.hosts
.as_ref()
.unwrap()
.len(),
1
);
assert_eq!(
networking.ingress.as_ref().unwrap().hosts.as_ref().unwrap()[0].host,
"api.example.com"
);
}
}
use std::collections::{BTreeMap, HashMap};