gitdataai/apps/operator/src/crd.rs
ZhenYi 3354055e6d fix(operator): mount /data PVC into git-hook deployment
GitHook controller was generating a Deployment without any persistent
storage — only a ConfigMap volume at /config. The worker needs /data to
access repo storage paths (APP_REPOS_ROOT defaults to /data/repos).

Changes:
- GitHookSpec: added storage_size field (default 10Gi), matching the
  pattern already used by GitServerSpec
- git_hook.rs reconcile(): now creates a PVC ({name}-data) before the
  Deployment, mounts it at /data, and sets APP_REPOS_ROOT=/data/repos
- git-hook-crd.yaml: synced storageSize field into the CRD schema
2026-04-17 14:15:38 +08:00

587 lines
16 KiB
Rust

//! Custom Resource Definitions (CRDs) — plain serde types.
//!
//! API Group: `code.dev`
//!
//! The operator watches these resources using `kube::Api::<MyCrd>::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<Value>
// ---------------------------------------------------------------------------
/// JsonResource wraps serde_json::Value and implements Resource so we can use
/// `kube::Api<JsonResource>` 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<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
self.body.serialize(s)
}
}
impl<'de> serde::Deserialize<'de> for JsonResource {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub value_from: Option<EnvVarSource>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvVarSource {
#[serde(skip_serializing_if = "Option::is_none")]
pub secret_ref: Option<SecretEnvVar>,
}
#[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<ResourceList>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limits: Option<ResourceList>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ResourceList {
#[serde(skip_serializing_if = "Option::is_none")]
pub cpu: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub memory: Option<String>,
}
#[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<EnvVar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourceRequirements>,
#[serde(skip_serializing_if = "Option::is_none")]
pub liveness_probe: Option<Probe>,
#[serde(skip_serializing_if = "Option::is_none")]
pub readiness_probe: Option<Probe>,
#[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<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
}
#[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<AppStatus>,
}
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<EnvVar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourceRequirements>,
#[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<String>,
#[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<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
}
#[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<GitServerStatus>,
}
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<EnvVar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourceRequirements>,
#[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<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
}
#[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<EmailWorkerStatus>,
}
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<EnvVar>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourceRequirements>,
#[serde(default)]
pub image_pull_policy: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub worker_id: Option<String>,
#[serde(default = "default_githook_storage_size")]
pub storage_size: String,
}
fn default_githook_image() -> String {
"myapp/git-hook:latest".to_string()
}
fn default_githook_storage_size() -> String {
"10Gi".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GitHookStatus {
#[serde(skip_serializing_if = "Option::is_none")]
pub ready_replicas: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phase: Option<String>,
}
#[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<GitHookStatus>,
}
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<EnvVar>,
#[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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_time: Option<String>,
}
#[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<MigrateStatus>,
}
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<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_owner_deletion: Option<bool>,
}
impl From<OwnerReference> 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,
}
}
}