⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/services/cri/
image.rs

1//! CRI Image Service
2//!
3//! Provides container image lifecycle management including pull, list,
4//! status, and removal operations.
5
6#![allow(dead_code)]
7
8use alloc::{collections::BTreeMap, string::String, vec::Vec};
9use core::sync::atomic::{AtomicU64, Ordering};
10
11// ---------------------------------------------------------------------------
12// Image Types
13// ---------------------------------------------------------------------------
14
15/// Image specification (reference to pull).
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ImageSpec {
18    /// Full image name (e.g., "docker.io/library/nginx").
19    pub image_name: String,
20    /// Image tag (e.g., "latest", "1.25").
21    pub tag: String,
22    /// Content-addressable digest (e.g., "sha256:abc...").
23    pub digest: String,
24}
25
26impl ImageSpec {
27    /// Create a new image spec.
28    pub fn new(image_name: String, tag: String) -> Self {
29        ImageSpec {
30            image_name,
31            tag,
32            digest: String::new(),
33        }
34    }
35
36    /// Create with digest.
37    pub fn with_digest(image_name: String, tag: String, digest: String) -> Self {
38        ImageSpec {
39            image_name,
40            tag,
41            digest,
42        }
43    }
44
45    /// Get the full reference string (name:tag).
46    pub fn reference(&self) -> String {
47        let mut r = self.image_name.clone();
48        r.push(':');
49        r.push_str(&self.tag);
50        r
51    }
52}
53
54/// Status of a pulled image.
55#[derive(Debug, Clone)]
56pub struct ImageStatus {
57    /// Unique image identifier.
58    pub id: u64,
59    /// Size in bytes.
60    pub size: u64,
61    /// Repository tags (e.g., ["nginx:latest", "nginx:1.25"]).
62    pub repo_tags: Vec<String>,
63    /// Repository digests.
64    pub repo_digests: Vec<String>,
65    /// Image spec that was pulled.
66    pub spec: ImageSpec,
67    /// Tick when the image was pulled.
68    pub pulled_tick: u64,
69}
70
71/// Authentication configuration for pulling images from private registries.
72#[derive(Debug, Clone, Default)]
73pub struct AuthConfig {
74    /// Username.
75    pub username: String,
76    /// Password or token.
77    pub password: String,
78    /// Registry server address.
79    pub server_address: String,
80}
81
82/// Image pull progress.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PullStatus {
85    /// Pull has not started.
86    Pending,
87    /// Downloading layers.
88    Downloading,
89    /// Extracting layers.
90    Extracting,
91    /// Pull complete.
92    Complete,
93    /// Pull failed.
94    Failed,
95}
96
97impl PullStatus {
98    /// Check if pull is terminal.
99    pub fn is_terminal(self) -> bool {
100        matches!(self, PullStatus::Complete | PullStatus::Failed)
101    }
102}
103
104/// Image service error.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum ImageError {
107    /// Image not found.
108    NotFound(String),
109    /// Image already exists.
110    AlreadyExists(String),
111    /// Registry authentication failed.
112    AuthenticationFailed,
113    /// Registry unreachable.
114    RegistryUnavailable,
115    /// Image pull failed.
116    PullFailed(String),
117}
118
119// ---------------------------------------------------------------------------
120// Image Service
121// ---------------------------------------------------------------------------
122
123/// Next image ID generator.
124static NEXT_IMAGE_ID: AtomicU64 = AtomicU64::new(1);
125
126fn alloc_image_id() -> u64 {
127    NEXT_IMAGE_ID.fetch_add(1, Ordering::Relaxed)
128}
129
130/// CRI ImageService implementation.
131#[derive(Debug)]
132pub struct ImageService {
133    /// Stored images keyed by ID.
134    images: BTreeMap<u64, ImageStatus>,
135    /// Index: image reference string -> image ID.
136    ref_index: BTreeMap<String, u64>,
137    /// Maximum total image storage (bytes).
138    max_storage: u64,
139    /// Current total storage used (bytes).
140    used_storage: u64,
141}
142
143impl Default for ImageService {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149impl ImageService {
150    /// Default maximum image storage: 10 GB.
151    pub const DEFAULT_MAX_STORAGE: u64 = 10 * 1024 * 1024 * 1024;
152
153    /// Create a new image service.
154    pub fn new() -> Self {
155        ImageService {
156            images: BTreeMap::new(),
157            ref_index: BTreeMap::new(),
158            max_storage: Self::DEFAULT_MAX_STORAGE,
159            used_storage: 0,
160        }
161    }
162
163    /// Create with a custom storage limit.
164    pub fn with_max_storage(max_storage: u64) -> Self {
165        ImageService {
166            images: BTreeMap::new(),
167            ref_index: BTreeMap::new(),
168            max_storage,
169            used_storage: 0,
170        }
171    }
172
173    /// Pull an image from a registry.
174    ///
175    /// In the kernel environment this is a conceptual operation that
176    /// registers the image metadata. Actual layer download would go
177    /// through the network stack and filesystem.
178    pub fn pull_image(
179        &mut self,
180        spec: ImageSpec,
181        _auth: Option<AuthConfig>,
182        current_tick: u64,
183    ) -> Result<u64, ImageError> {
184        let reference = spec.reference();
185
186        // Check if already pulled
187        if let Some(&existing_id) = self.ref_index.get(&reference) {
188            return Ok(existing_id);
189        }
190
191        // Simulate a pulled image with estimated size
192        let estimated_size = self.estimate_image_size(&spec);
193
194        if self.used_storage.saturating_add(estimated_size) > self.max_storage {
195            return Err(ImageError::PullFailed(String::from(
196                "storage limit exceeded",
197            )));
198        }
199
200        let id = alloc_image_id();
201        let digest = self.compute_digest(&spec);
202
203        let status = ImageStatus {
204            id,
205            size: estimated_size,
206            repo_tags: alloc::vec![reference.clone()],
207            repo_digests: alloc::vec![digest],
208            spec,
209            pulled_tick: current_tick,
210        };
211
212        self.used_storage = self.used_storage.saturating_add(estimated_size);
213        self.ref_index.insert(reference, id);
214        self.images.insert(id, status);
215
216        Ok(id)
217    }
218
219    /// List all images.
220    pub fn list_images(&self) -> Vec<&ImageStatus> {
221        self.images.values().collect()
222    }
223
224    /// Get image status by ID.
225    pub fn image_status(&self, image_id: u64) -> Option<&ImageStatus> {
226        self.images.get(&image_id)
227    }
228
229    /// Get image status by reference string.
230    pub fn image_status_by_ref(&self, reference: &str) -> Option<&ImageStatus> {
231        self.ref_index
232            .get(reference)
233            .and_then(|id| self.images.get(id))
234    }
235
236    /// Remove an image.
237    pub fn remove_image(&mut self, image_id: u64) -> Result<(), ImageError> {
238        let image = self
239            .images
240            .remove(&image_id)
241            .ok_or_else(|| ImageError::NotFound(alloc::format!("id={}", image_id)))?;
242
243        self.used_storage = self.used_storage.saturating_sub(image.size);
244
245        // Remove all ref index entries pointing to this image
246        self.ref_index.retain(|_, id| *id != image_id);
247
248        Ok(())
249    }
250
251    /// Remove an image by reference string.
252    pub fn remove_image_by_ref(&mut self, reference: &str) -> Result<(), ImageError> {
253        let image_id = *self
254            .ref_index
255            .get(reference)
256            .ok_or_else(|| ImageError::NotFound(String::from(reference)))?;
257        self.remove_image(image_id)
258    }
259
260    /// Get the total number of images.
261    pub fn image_count(&self) -> usize {
262        self.images.len()
263    }
264
265    /// Get total storage used.
266    pub fn used_storage(&self) -> u64 {
267        self.used_storage
268    }
269
270    /// Get available storage.
271    pub fn available_storage(&self) -> u64 {
272        self.max_storage.saturating_sub(self.used_storage)
273    }
274
275    /// Estimate image size from spec (deterministic pseudo-hash).
276    fn estimate_image_size(&self, spec: &ImageSpec) -> u64 {
277        // Deterministic size based on name length
278        let base = 50 * 1024 * 1024; // 50 MB base
279        let name_factor = spec.image_name.len() as u64 * 1024 * 1024;
280        base + name_factor
281    }
282
283    /// Compute a deterministic digest from spec.
284    fn compute_digest(&self, spec: &ImageSpec) -> String {
285        // Simple deterministic digest for testing
286        let mut hash: u64 = 0x5381;
287        for b in spec.image_name.bytes() {
288            hash = hash.wrapping_mul(33).wrapping_add(b as u64);
289        }
290        for b in spec.tag.bytes() {
291            hash = hash.wrapping_mul(33).wrapping_add(b as u64);
292        }
293        alloc::format!(
294            "sha256:{:016x}{:016x}{:016x}{:016x}",
295            hash,
296            hash ^ 0xFF,
297            hash.wrapping_mul(7),
298            hash.wrapping_add(42)
299        )
300    }
301}
302
303// ---------------------------------------------------------------------------
304// Tests
305// ---------------------------------------------------------------------------
306
307#[cfg(test)]
308mod tests {
309    #[allow(unused_imports)]
310    use alloc::string::ToString;
311
312    use super::*;
313
314    fn make_service() -> ImageService {
315        ImageService::new()
316    }
317
318    fn test_spec() -> ImageSpec {
319        ImageSpec::new(String::from("nginx"), String::from("latest"))
320    }
321
322    #[test]
323    fn test_pull_image() {
324        let mut svc = make_service();
325        let id = svc.pull_image(test_spec(), None, 100).unwrap();
326        assert!(id > 0);
327        assert_eq!(svc.image_count(), 1);
328    }
329
330    #[test]
331    fn test_pull_duplicate_returns_existing() {
332        let mut svc = make_service();
333        let id1 = svc.pull_image(test_spec(), None, 100).unwrap();
334        let id2 = svc.pull_image(test_spec(), None, 200).unwrap();
335        assert_eq!(id1, id2);
336        assert_eq!(svc.image_count(), 1);
337    }
338
339    #[test]
340    fn test_image_status() {
341        let mut svc = make_service();
342        let id = svc.pull_image(test_spec(), None, 100).unwrap();
343        let status = svc.image_status(id).unwrap();
344        assert_eq!(status.spec.image_name, "nginx");
345        assert_eq!(status.pulled_tick, 100);
346    }
347
348    #[test]
349    fn test_image_status_by_ref() {
350        let mut svc = make_service();
351        svc.pull_image(test_spec(), None, 100).unwrap();
352        let status = svc.image_status_by_ref("nginx:latest").unwrap();
353        assert_eq!(status.spec.tag, "latest");
354    }
355
356    #[test]
357    fn test_list_images() {
358        let mut svc = make_service();
359        svc.pull_image(test_spec(), None, 100).unwrap();
360        svc.pull_image(
361            ImageSpec::new(String::from("redis"), String::from("7")),
362            None,
363            200,
364        )
365        .unwrap();
366        assert_eq!(svc.list_images().len(), 2);
367    }
368
369    #[test]
370    fn test_remove_image() {
371        let mut svc = make_service();
372        let id = svc.pull_image(test_spec(), None, 100).unwrap();
373        let storage_before = svc.used_storage();
374        svc.remove_image(id).unwrap();
375        assert_eq!(svc.image_count(), 0);
376        assert!(svc.used_storage() < storage_before);
377    }
378
379    #[test]
380    fn test_remove_image_by_ref() {
381        let mut svc = make_service();
382        svc.pull_image(test_spec(), None, 100).unwrap();
383        svc.remove_image_by_ref("nginx:latest").unwrap();
384        assert_eq!(svc.image_count(), 0);
385    }
386
387    #[test]
388    fn test_storage_limit() {
389        let mut svc = ImageService::with_max_storage(1024); // Very small
390        let result = svc.pull_image(test_spec(), None, 100);
391        assert!(result.is_err());
392    }
393}