Skip to main content

modelexpress_server/registry/
k8s_types.rs

1// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Kubernetes CRD types for the model registry.
5//!
6//! Defines `ModelCacheEntry` — a custom resource that holds model-download lifecycle
7//! state (phase + timestamps + message) for one cached model. This is the registry
8//! analogue of the P2P `ModelMetadata` CRD, kept in a separate Kind so cardinality,
9//! lifecycle, and RBAC scopes stay independent.
10
11use kube::CustomResource;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14
15/// ModelCacheEntry spec - desired state. One CR per cached model.
16#[derive(CustomResource, Clone, Debug, Deserialize, Serialize, JsonSchema)]
17#[kube(
18    group = "modelexpress.nvidia.com",
19    version = "v1alpha1",
20    kind = "ModelCacheEntry",
21    plural = "modelcacheentries",
22    shortname = "mxcache",
23    namespaced,
24    status = "ModelCacheEntryStatus"
25)]
26pub struct ModelCacheEntrySpec {
27    /// Full model name (e.g., `meta-llama/Llama-3.1-70B`). Preserved in spec so
28    /// operators can recover the original name even when the CR's metadata.name is a
29    /// sanitized/hashed form.
30    #[serde(rename = "modelName")]
31    pub model_name: String,
32
33    /// Provider string — `"HuggingFace"`, `"Ngc"`, or `"Gcs"`.
34    pub provider: String,
35}
36
37/// ModelCacheEntry status - observed state.
38#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema)]
39pub struct ModelCacheEntryStatus {
40    /// One of `"Downloading"`, `"Downloaded"`, `"Error"`. Empty string on freshly-created
41    /// records that haven't yet received a status patch.
42    #[serde(default)]
43    pub phase: String,
44
45    /// RFC3339 timestamp of first write. Omitted until the first status patch.
46    #[serde(rename = "createdAt", default)]
47    pub created_at: Option<String>,
48
49    /// RFC3339 timestamp of most recent status write or touch.
50    #[serde(rename = "lastUsedAt", default)]
51    pub last_used_at: Option<String>,
52
53    /// Optional human-readable message (download progress, error reason).
54    #[serde(default)]
55    pub message: Option<String>,
56}
57
58/// Phase strings used by the CRD. Match `ModelStatus` one-for-one.
59pub mod phase {
60    pub const DOWNLOADING: &str = "Downloading";
61    pub const DOWNLOADED: &str = "Downloaded";
62    pub const ERROR: &str = "Error";
63}
64
65#[cfg(test)]
66#[allow(clippy::expect_used)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn spec_roundtrips_through_json() {
72        let spec = ModelCacheEntrySpec {
73            model_name: "meta-llama/Llama-3.1-70B".to_string(),
74            provider: "HuggingFace".to_string(),
75        };
76        let json = serde_json::to_string(&spec).expect("serialize");
77        assert!(json.contains("\"modelName\":\"meta-llama/Llama-3.1-70B\""));
78        let back: ModelCacheEntrySpec = serde_json::from_str(&json).expect("deserialize");
79        assert_eq!(back.model_name, spec.model_name);
80        assert_eq!(back.provider, spec.provider);
81    }
82
83    #[test]
84    fn status_default_is_empty() {
85        let status = ModelCacheEntryStatus::default();
86        assert_eq!(status.phase, "");
87        assert!(status.created_at.is_none());
88        assert!(status.last_used_at.is_none());
89        assert!(status.message.is_none());
90    }
91}