use serde::{Deserialize, Serialize};
use super::keda::KedaContract;
use super::native_deps::NativeDepsContract;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ImageProfile {
#[default]
Production,
Development,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeploymentContract {
#[serde(default = "default_schema_version")]
pub schema_version: u32,
pub app_name: String,
#[serde(default)]
pub binary_name: String,
#[serde(default)]
pub description: String,
pub metrics_port: u16,
pub health: HealthContract,
pub env_prefix: String,
pub metric_prefix: String,
pub config_mount_path: String,
#[serde(default = "default_image_registry")]
pub image_registry: String,
#[serde(default)]
pub extra_ports: Vec<PortContract>,
#[serde(default)]
pub entrypoint_args: Vec<String>,
#[serde(default)]
pub secrets: Vec<SecretGroupContract>,
#[serde(default)]
pub default_config: Option<serde_json::Value>,
#[serde(default)]
pub depends_on: Vec<String>,
pub keda: Option<KedaContract>,
#[serde(default = "default_base_image")]
pub base_image: String,
#[serde(default)]
pub native_deps: NativeDepsContract,
#[serde(default)]
pub image_profile: ImageProfile,
#[serde(default)]
pub oci_labels: OciLabels,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OciLabels {
#[serde(default)]
pub title: String,
#[serde(default)]
pub description: String,
#[serde(default = "default_vendor")]
pub vendor: String,
#[serde(default = "default_license")]
pub licenses: String,
}
impl Default for OciLabels {
fn default() -> Self {
Self {
title: String::new(),
description: String::new(),
vendor: default_vendor(),
licenses: default_license(),
}
}
}
fn default_vendor() -> String {
"HYPERI PTY LIMITED".to_string()
}
fn default_license() -> String {
"FSL-1.1-ALv2".to_string()
}
fn default_schema_version() -> u32 {
2
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthContract {
pub liveness_path: String,
pub readiness_path: String,
pub metrics_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PortContract {
pub name: String,
pub port: u16,
#[serde(default = "default_protocol")]
pub protocol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretGroupContract {
pub group_name: String,
pub env_vars: Vec<SecretEnvContract>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretEnvContract {
pub env_var: String,
pub key_name: String,
pub secret_key: String,
}
fn default_base_image() -> String {
"ubuntu:24.04".to_string()
}
fn default_image_registry() -> String {
"ghcr.io/hyperi-io".to_string()
}
fn default_protocol() -> String {
"TCP".to_string()
}
impl DeploymentContract {
#[must_use]
pub fn binary(&self) -> &str {
if self.binary_name.is_empty() {
&self.app_name
} else {
&self.binary_name
}
}
#[must_use]
pub fn config_filename(&self) -> &str {
self.config_mount_path
.rsplit('/')
.next()
.unwrap_or("config.yaml")
}
#[must_use]
pub fn config_dir(&self) -> &str {
self.config_mount_path
.rsplit_once('/')
.map_or("/etc", |(dir, _)| dir)
}
#[must_use]
pub fn to_json(&self) -> String {
serde_json::to_string_pretty(self).unwrap_or_default()
}
#[must_use]
pub fn to_yaml(&self) -> String {
serde_yaml_ng::to_string(self).unwrap_or_default()
}
#[must_use]
pub fn with_dev_profile(&self) -> Self {
let mut dev = self.clone();
dev.image_profile = ImageProfile::Development;
dev
}
}
impl Default for HealthContract {
fn default() -> Self {
Self {
liveness_path: "/healthz".to_string(),
readiness_path: "/readyz".to_string(),
metrics_path: "/metrics".to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_health_contract_defaults() {
let h = HealthContract::default();
assert_eq!(h.liveness_path, "/healthz");
assert_eq!(h.readiness_path, "/readyz");
assert_eq!(h.metrics_path, "/metrics");
}
#[test]
fn test_contract_to_json() {
let contract = DeploymentContract {
app_name: "test-app".into(),
metrics_port: 9090,
health: HealthContract::default(),
env_prefix: "TEST_APP".into(),
metric_prefix: "test".into(),
config_mount_path: "/etc/test/config.yaml".into(),
keda: None,
binary_name: String::new(),
description: String::new(),
image_registry: default_image_registry(),
extra_ports: vec![],
entrypoint_args: vec![],
secrets: vec![],
default_config: None,
depends_on: vec![],
base_image: "ubuntu:24.04".into(),
native_deps: NativeDepsContract::default(),
image_profile: ImageProfile::default(),
schema_version: 2,
oci_labels: OciLabels::default(),
};
let json = contract.to_json();
assert!(json.contains("test-app"));
assert!(json.contains("9090"));
}
#[test]
fn test_contract_roundtrip_json() {
let contract = DeploymentContract {
app_name: "roundtrip".into(),
metrics_port: 8080,
health: HealthContract::default(),
env_prefix: "RT".into(),
metric_prefix: "rt".into(),
config_mount_path: "/config.yaml".into(),
keda: None,
binary_name: String::new(),
description: String::new(),
image_registry: default_image_registry(),
extra_ports: vec![],
entrypoint_args: vec![],
secrets: vec![],
default_config: None,
depends_on: vec![],
base_image: "ubuntu:24.04".into(),
native_deps: NativeDepsContract::default(),
image_profile: ImageProfile::default(),
schema_version: 2,
oci_labels: OciLabels::default(),
};
let json = contract.to_json();
let parsed: DeploymentContract = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.app_name, "roundtrip");
assert_eq!(parsed.metrics_port, 8080);
}
#[test]
fn test_binary_name_fallback() {
let contract = DeploymentContract {
app_name: "my-app".into(),
binary_name: String::new(),
metrics_port: 9090,
health: HealthContract::default(),
env_prefix: "MY_APP".into(),
metric_prefix: "app".into(),
config_mount_path: "/etc/app/config.yaml".into(),
keda: None,
description: String::new(),
image_registry: default_image_registry(),
extra_ports: vec![],
entrypoint_args: vec![],
secrets: vec![],
default_config: None,
depends_on: vec![],
base_image: "ubuntu:24.04".into(),
native_deps: NativeDepsContract::default(),
image_profile: ImageProfile::default(),
schema_version: 2,
oci_labels: OciLabels::default(),
};
assert_eq!(contract.binary(), "my-app");
}
#[test]
fn test_config_filename() {
let contract = DeploymentContract {
app_name: "test".into(),
config_mount_path: "/etc/dfe/loader.yaml".into(),
metrics_port: 9090,
health: HealthContract::default(),
env_prefix: "T".into(),
metric_prefix: "t".into(),
keda: None,
binary_name: String::new(),
description: String::new(),
image_registry: default_image_registry(),
extra_ports: vec![],
entrypoint_args: vec![],
secrets: vec![],
default_config: None,
depends_on: vec![],
base_image: "ubuntu:24.04".into(),
native_deps: NativeDepsContract::default(),
image_profile: ImageProfile::default(),
schema_version: 2,
oci_labels: OciLabels::default(),
};
assert_eq!(contract.config_filename(), "loader.yaml");
assert_eq!(contract.config_dir(), "/etc/dfe");
}
}