//! Custom Resource Definitions (CRDs) — plain serde types. //! //! API Group: `code.dev` //! //! The operator watches these resources using `kube::Api::::all(client)`. //! Reconcile is triggered on every change to any instance of these types. use k8s_openapi::apimachinery::pkg::apis::meta::v1::{ ObjectMeta, OwnerReference as K8sOwnerReference, }; use kube::Resource; use serde::{Deserialize, Serialize}; use std::borrow::Cow; // --------------------------------------------------------------------------- // A dynamic Resource impl for serde_json::Value — lets us use kube::Api // --------------------------------------------------------------------------- /// JsonResource wraps serde_json::Value and implements Resource so we can use /// `kube::Api` for arbitrary child-resource API calls. /// The metadata field is kept separate to satisfy the Resource::meta() bound. #[derive(Clone, Debug, Default)] pub struct JsonResource { meta: ObjectMeta, body: serde_json::Value, } impl JsonResource { pub fn new(meta: ObjectMeta, body: serde_json::Value) -> Self { JsonResource { meta, body } } } impl std::ops::Deref for JsonResource { type Target = serde_json::Value; fn deref(&self) -> &serde_json::Value { &self.body } } impl serde::Serialize for JsonResource { fn serialize(&self, s: S) -> Result { self.body.serialize(s) } } impl<'de> serde::Deserialize<'de> for JsonResource { fn deserialize>(d: D) -> Result { let body = serde_json::Value::deserialize(d)?; let meta = body .get("metadata") .and_then(|m| serde_json::from_value(m.clone()).ok()) .unwrap_or_default(); Ok(JsonResource { meta, body }) } } impl Resource for JsonResource { type DynamicType = (); type Scope = k8s_openapi::NamespaceResourceScope; fn kind(_: &()) -> Cow<'_, str> { Cow::Borrowed("Object") } fn group(_: &()) -> Cow<'_, str> { Cow::Borrowed("") } fn version(_: &()) -> Cow<'_, str> { Cow::Borrowed("v1") } fn plural(_: &()) -> Cow<'_, str> { Cow::Borrowed("objects") } fn meta(&self) -> &ObjectMeta { &self.meta } fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.meta } } // --------------------------------------------------------------------------- // Shared types // --------------------------------------------------------------------------- /// EnvVar with optional secret reference. #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EnvVar { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub value: Option, #[serde(skip_serializing_if = "Option::is_none")] pub value_from: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EnvVarSource { #[serde(skip_serializing_if = "Option::is_none")] pub secret_ref: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct SecretEnvVar { pub name: String, pub secret_name: String, pub secret_key: String, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ResourceRequirements { #[serde(skip_serializing_if = "Option::is_none")] pub requests: Option, #[serde(skip_serializing_if = "Option::is_none")] pub limits: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ResourceList { #[serde(skip_serializing_if = "Option::is_none")] pub cpu: Option, #[serde(skip_serializing_if = "Option::is_none")] pub memory: Option, } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Probe { #[serde(default = "default_port")] pub port: i32, #[serde(default = "default_path")] pub path: String, #[serde(default = "default_initial_delay")] pub initial_delay_seconds: i32, } fn default_port() -> i32 { 8080 } fn default_path() -> String { "/health".to_string() } fn default_initial_delay() -> i32 { 5 } // --------------------------------------------------------------------------- // App CRD // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AppSpec { #[serde(default = "default_app_image")] pub image: String, #[serde(default = "default_replicas")] pub replicas: i32, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub env: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option, #[serde(skip_serializing_if = "Option::is_none")] pub liveness_probe: Option, #[serde(skip_serializing_if = "Option::is_none")] pub readiness_probe: Option, #[serde(default)] pub image_pull_policy: String, } fn default_app_image() -> String { "myapp/app:latest".to_string() } fn default_replicas() -> i32 { 3 } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AppStatus { #[serde(skip_serializing_if = "Option::is_none")] pub ready_replicas: Option, #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct App { pub api_version: String, pub kind: String, pub metadata: K8sObjectMeta, pub spec: AppSpec, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } impl App { pub fn api_group() -> &'static str { "code.dev" } pub fn version() -> &'static str { "v1" } pub fn plural() -> &'static str { "apps" } } impl Resource for App { type DynamicType = (); type Scope = k8s_openapi::NamespaceResourceScope; fn kind(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("App") } fn group(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("code.dev") } fn version(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("v1") } fn plural(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("apps") } fn meta(&self) -> &ObjectMeta { &self.metadata } fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } } // --------------------------------------------------------------------------- // GitServer CRD // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitServerSpec { #[serde(default = "default_gitserver_image")] pub image: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub env: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option, #[serde(default = "default_ssh_service_type")] pub ssh_service_type: String, #[serde(default = "default_storage_size")] pub storage_size: String, #[serde(default)] pub image_pull_policy: String, #[serde(skip_serializing_if = "Option::is_none")] pub ssh_domain: Option, #[serde(default = "default_ssh_port")] pub ssh_port: i32, #[serde(default = "default_http_port")] pub http_port: i32, } fn default_gitserver_image() -> String { "myapp/gitserver:latest".to_string() } fn default_ssh_service_type() -> String { "NodePort".to_string() } fn default_storage_size() -> String { "10Gi".to_string() } fn default_ssh_port() -> i32 { 22 } fn default_http_port() -> i32 { 8022 } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct GitServerStatus { #[serde(skip_serializing_if = "Option::is_none")] pub ready_replicas: Option, #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitServer { pub api_version: String, pub kind: String, pub metadata: K8sObjectMeta, pub spec: GitServerSpec, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } impl GitServer { pub fn api_group() -> &'static str { "code.dev" } pub fn version() -> &'static str { "v1" } pub fn plural() -> &'static str { "gitservers" } } impl Resource for GitServer { type DynamicType = (); type Scope = k8s_openapi::NamespaceResourceScope; fn kind(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("GitServer") } fn group(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("code.dev") } fn version(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("v1") } fn plural(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("gitservers") } fn meta(&self) -> &ObjectMeta { &self.metadata } fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } } // --------------------------------------------------------------------------- // EmailWorker CRD // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailWorkerSpec { #[serde(default = "default_email_image")] pub image: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub env: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option, #[serde(default)] pub image_pull_policy: String, } fn default_email_image() -> String { "myapp/email-worker:latest".to_string() } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct EmailWorkerStatus { #[serde(skip_serializing_if = "Option::is_none")] pub ready_replicas: Option, #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EmailWorker { pub api_version: String, pub kind: String, pub metadata: K8sObjectMeta, pub spec: EmailWorkerSpec, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } impl EmailWorker { pub fn api_group() -> &'static str { "code.dev" } pub fn version() -> &'static str { "v1" } pub fn plural() -> &'static str { "emailworkers" } } impl Resource for EmailWorker { type DynamicType = (); type Scope = k8s_openapi::NamespaceResourceScope; fn kind(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("EmailWorker") } fn group(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("code.dev") } fn version(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("v1") } fn plural(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("emailworkers") } fn meta(&self) -> &ObjectMeta { &self.metadata } fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } } // --------------------------------------------------------------------------- // GitHook CRD // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitHookSpec { #[serde(default = "default_githook_image")] pub image: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub env: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub resources: Option, #[serde(default)] pub image_pull_policy: String, #[serde(skip_serializing_if = "Option::is_none")] pub worker_id: Option, } fn default_githook_image() -> String { "myapp/git-hook:latest".to_string() } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct GitHookStatus { #[serde(skip_serializing_if = "Option::is_none")] pub ready_replicas: Option, #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitHook { pub api_version: String, pub kind: String, pub metadata: K8sObjectMeta, pub spec: GitHookSpec, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } impl GitHook { pub fn api_group() -> &'static str { "code.dev" } pub fn version() -> &'static str { "v1" } pub fn plural() -> &'static str { "githooks" } } impl Resource for GitHook { type DynamicType = (); type Scope = k8s_openapi::NamespaceResourceScope; fn kind(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("GitHook") } fn group(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("code.dev") } fn version(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("v1") } fn plural(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("githooks") } fn meta(&self) -> &ObjectMeta { &self.metadata } fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } } // --------------------------------------------------------------------------- // Migrate CRD // --------------------------------------------------------------------------- #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MigrateSpec { #[serde(default = "default_migrate_image")] pub image: String, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub env: Vec, #[serde(default = "default_migrate_cmd")] pub command: String, #[serde(default = "default_backoff_limit")] pub backoff_limit: i32, } fn default_migrate_image() -> String { "myapp/migrate:latest".to_string() } fn default_migrate_cmd() -> String { "up".to_string() } fn default_backoff_limit() -> i32 { 3 } #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct MigrateStatus { #[serde(skip_serializing_if = "Option::is_none")] pub phase: Option, #[serde(skip_serializing_if = "Option::is_none")] pub start_time: Option, #[serde(skip_serializing_if = "Option::is_none")] pub completion_time: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Migrate { pub api_version: String, pub kind: String, pub metadata: K8sObjectMeta, pub spec: MigrateSpec, #[serde(skip_serializing_if = "Option::is_none")] pub status: Option, } impl Migrate { pub fn api_group() -> &'static str { "code.dev" } pub fn version() -> &'static str { "v1" } pub fn plural() -> &'static str { "migrates" } } impl Resource for Migrate { type DynamicType = (); type Scope = k8s_openapi::NamespaceResourceScope; fn kind(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("Migrate") } fn group(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("code.dev") } fn version(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("v1") } fn plural(_: &Self::DynamicType) -> Cow<'_, str> { Cow::Borrowed("migrates") } fn meta(&self) -> &ObjectMeta { &self.metadata } fn meta_mut(&mut self) -> &mut ObjectMeta { &mut self.metadata } } // --------------------------------------------------------------------------- // Shared K8s types — aligned with k8s-openapi for Resource trait compatibility // --------------------------------------------------------------------------- /// Type alias so K8sObjectMeta satisfies Resource::meta() -> &k8s_openapi::...::ObjectMeta. pub type K8sObjectMeta = ObjectMeta; /// OwnerReference compatible with k8s-openapi. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OwnerReference { pub api_version: String, pub kind: String, pub name: String, pub uid: String, #[serde(default, skip_serializing_if = "Option::is_none")] pub controller: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub block_owner_deletion: Option, } impl From for K8sOwnerReference { fn from(o: OwnerReference) -> Self { K8sOwnerReference { api_version: o.api_version, kind: o.kind, name: o.name, uid: o.uid, controller: o.controller, block_owner_deletion: o.block_owner_deletion, } } }