veridian_kernel/services/cri/
image.rs1#![allow(dead_code)]
7
8use alloc::{collections::BTreeMap, string::String, vec::Vec};
9use core::sync::atomic::{AtomicU64, Ordering};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ImageSpec {
18 pub image_name: String,
20 pub tag: String,
22 pub digest: String,
24}
25
26impl ImageSpec {
27 pub fn new(image_name: String, tag: String) -> Self {
29 ImageSpec {
30 image_name,
31 tag,
32 digest: String::new(),
33 }
34 }
35
36 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 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#[derive(Debug, Clone)]
56pub struct ImageStatus {
57 pub id: u64,
59 pub size: u64,
61 pub repo_tags: Vec<String>,
63 pub repo_digests: Vec<String>,
65 pub spec: ImageSpec,
67 pub pulled_tick: u64,
69}
70
71#[derive(Debug, Clone, Default)]
73pub struct AuthConfig {
74 pub username: String,
76 pub password: String,
78 pub server_address: String,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum PullStatus {
85 Pending,
87 Downloading,
89 Extracting,
91 Complete,
93 Failed,
95}
96
97impl PullStatus {
98 pub fn is_terminal(self) -> bool {
100 matches!(self, PullStatus::Complete | PullStatus::Failed)
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum ImageError {
107 NotFound(String),
109 AlreadyExists(String),
111 AuthenticationFailed,
113 RegistryUnavailable,
115 PullFailed(String),
117}
118
119static 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#[derive(Debug)]
132pub struct ImageService {
133 images: BTreeMap<u64, ImageStatus>,
135 ref_index: BTreeMap<String, u64>,
137 max_storage: u64,
139 used_storage: u64,
141}
142
143impl Default for ImageService {
144 fn default() -> Self {
145 Self::new()
146 }
147}
148
149impl ImageService {
150 pub const DEFAULT_MAX_STORAGE: u64 = 10 * 1024 * 1024 * 1024;
152
153 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 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 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 if let Some(&existing_id) = self.ref_index.get(&reference) {
188 return Ok(existing_id);
189 }
190
191 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 pub fn list_images(&self) -> Vec<&ImageStatus> {
221 self.images.values().collect()
222 }
223
224 pub fn image_status(&self, image_id: u64) -> Option<&ImageStatus> {
226 self.images.get(&image_id)
227 }
228
229 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 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 self.ref_index.retain(|_, id| *id != image_id);
247
248 Ok(())
249 }
250
251 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 pub fn image_count(&self) -> usize {
262 self.images.len()
263 }
264
265 pub fn used_storage(&self) -> u64 {
267 self.used_storage
268 }
269
270 pub fn available_storage(&self) -> u64 {
272 self.max_storage.saturating_sub(self.used_storage)
273 }
274
275 fn estimate_image_size(&self, spec: &ImageSpec) -> u64 {
277 let base = 50 * 1024 * 1024; let name_factor = spec.image_name.len() as u64 * 1024 * 1024;
280 base + name_factor
281 }
282
283 fn compute_digest(&self, spec: &ImageSpec) -> String {
285 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#[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); let result = svc.pull_image(test_spec(), None, 100);
391 assert!(result.is_err());
392 }
393}