1use crate::api::generated::common::ContainerdNamespace as ProtoContainerdNamespace;
21use crate::api::generated::machine::{
22 ImageListRequest as ProtoImageListRequest, ImageListResponse as ProtoImageListResponse,
23 ImagePull as ProtoImagePull, ImagePullRequest as ProtoImagePullRequest,
24 ImagePullResponse as ProtoImagePullResponse,
25};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
33pub enum ContainerdNamespace {
34 Unknown,
36 #[default]
38 System,
39 Cri,
41}
42
43impl ContainerdNamespace {
44 #[must_use]
46 pub fn as_proto_i32(&self) -> i32 {
47 match self {
48 Self::Unknown => ProtoContainerdNamespace::NsUnknown as i32,
49 Self::System => ProtoContainerdNamespace::NsSystem as i32,
50 Self::Cri => ProtoContainerdNamespace::NsCri as i32,
51 }
52 }
53}
54
55impl From<i32> for ContainerdNamespace {
56 fn from(value: i32) -> Self {
57 match value {
58 x if x == ProtoContainerdNamespace::NsSystem as i32 => Self::System,
59 x if x == ProtoContainerdNamespace::NsCri as i32 => Self::Cri,
60 _ => Self::Unknown,
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default)]
71pub struct ImageListRequest {
72 pub namespace: ContainerdNamespace,
74}
75
76impl ImageListRequest {
77 #[must_use]
79 pub fn new(namespace: ContainerdNamespace) -> Self {
80 Self { namespace }
81 }
82
83 #[must_use]
85 pub fn system() -> Self {
86 Self::new(ContainerdNamespace::System)
87 }
88
89 #[must_use]
91 pub fn cri() -> Self {
92 Self::new(ContainerdNamespace::Cri)
93 }
94}
95
96impl From<ImageListRequest> for ProtoImageListRequest {
97 fn from(req: ImageListRequest) -> Self {
98 Self {
99 namespace: req.namespace.as_proto_i32(),
100 }
101 }
102}
103
104#[derive(Debug, Clone)]
110pub struct ImageInfo {
111 pub node: Option<String>,
113 pub name: String,
115 pub digest: String,
117 pub size: i64,
119 pub created_at: Option<prost_types::Timestamp>,
121}
122
123impl ImageInfo {
124 #[must_use]
126 pub fn size_mb(&self) -> f64 {
127 self.size as f64 / 1_048_576.0
128 }
129
130 #[must_use]
132 pub fn size_human(&self) -> String {
133 let size = self.size as f64;
134 if size < 1024.0 {
135 format!("{:.0} B", size)
136 } else if size < 1_048_576.0 {
137 format!("{:.1} KB", size / 1024.0)
138 } else if size < 1_073_741_824.0 {
139 format!("{:.1} MB", size / 1_048_576.0)
140 } else {
141 format!("{:.2} GB", size / 1_073_741_824.0)
142 }
143 }
144
145 #[must_use]
147 pub fn is_digest_reference(&self) -> bool {
148 self.name.contains('@')
149 }
150
151 #[must_use]
153 pub fn repository(&self) -> &str {
154 if let Some(pos) = self.name.find('@') {
155 &self.name[..pos]
156 } else if let Some(pos) = self.name.rfind(':') {
157 let before_colon = &self.name[..pos];
159 if before_colon.contains('/') || !before_colon.contains('.') {
160 &self.name[..pos]
161 } else {
162 &self.name
163 }
164 } else {
165 &self.name
166 }
167 }
168
169 #[must_use]
171 pub fn tag(&self) -> Option<&str> {
172 if self.name.contains('@') {
173 return None;
174 }
175 if let Some(pos) = self.name.rfind(':') {
176 let before_colon = &self.name[..pos];
177 if before_colon.contains('/') || !before_colon.contains('.') {
179 return Some(&self.name[pos + 1..]);
180 }
181 }
182 None
183 }
184}
185
186impl From<ProtoImageListResponse> for ImageInfo {
187 fn from(proto: ProtoImageListResponse) -> Self {
188 Self {
189 node: proto.metadata.map(|m| m.hostname),
190 name: proto.name,
191 digest: proto.digest,
192 size: proto.size,
193 created_at: proto.created_at,
194 }
195 }
196}
197
198#[derive(Debug, Clone)]
204pub struct ImagePullRequest {
205 pub namespace: ContainerdNamespace,
207 pub reference: String,
209}
210
211impl ImagePullRequest {
212 #[must_use]
216 pub fn new(reference: impl Into<String>) -> Self {
217 Self {
218 namespace: ContainerdNamespace::System,
219 reference: reference.into(),
220 }
221 }
222
223 #[must_use]
225 pub fn with_namespace(mut self, namespace: ContainerdNamespace) -> Self {
226 self.namespace = namespace;
227 self
228 }
229
230 #[must_use]
232 pub fn for_cri(mut self) -> Self {
233 self.namespace = ContainerdNamespace::Cri;
234 self
235 }
236}
237
238impl From<ImagePullRequest> for ProtoImagePullRequest {
239 fn from(req: ImagePullRequest) -> Self {
240 Self {
241 namespace: req.namespace.as_proto_i32(),
242 reference: req.reference,
243 }
244 }
245}
246
247#[derive(Debug, Clone)]
253pub struct ImagePullResult {
254 pub node: Option<String>,
256}
257
258impl From<ProtoImagePull> for ImagePullResult {
259 fn from(proto: ProtoImagePull) -> Self {
260 Self {
261 node: proto.metadata.map(|m| m.hostname),
262 }
263 }
264}
265
266#[derive(Debug, Clone)]
268pub struct ImagePullResponse {
269 pub results: Vec<ImagePullResult>,
271}
272
273impl ImagePullResponse {
274 #[must_use]
276 pub fn all_succeeded(&self) -> bool {
277 !self.results.is_empty()
278 }
279
280 #[must_use]
282 pub fn nodes(&self) -> Vec<&str> {
283 self.results
284 .iter()
285 .filter_map(|r| r.node.as_deref())
286 .collect()
287 }
288}
289
290impl From<ProtoImagePullResponse> for ImagePullResponse {
291 fn from(proto: ProtoImagePullResponse) -> Self {
292 Self {
293 results: proto.messages.into_iter().map(Into::into).collect(),
294 }
295 }
296}
297
298#[cfg(test)]
303mod tests {
304 use super::*;
305
306 #[test]
307 fn test_containerd_namespace_default() {
308 let ns = ContainerdNamespace::default();
309 assert_eq!(ns, ContainerdNamespace::System);
310 }
311
312 #[test]
313 fn test_containerd_namespace_from_i32() {
314 assert_eq!(ContainerdNamespace::from(0), ContainerdNamespace::Unknown);
315 assert_eq!(ContainerdNamespace::from(1), ContainerdNamespace::System);
316 assert_eq!(ContainerdNamespace::from(2), ContainerdNamespace::Cri);
317 assert_eq!(ContainerdNamespace::from(999), ContainerdNamespace::Unknown);
318 }
319
320 #[test]
321 fn test_image_list_request_constructors() {
322 let req = ImageListRequest::system();
323 assert_eq!(req.namespace, ContainerdNamespace::System);
324
325 let req = ImageListRequest::cri();
326 assert_eq!(req.namespace, ContainerdNamespace::Cri);
327 }
328
329 #[test]
330 fn test_image_list_request_to_proto() {
331 let req = ImageListRequest::cri();
332 let proto: ProtoImageListRequest = req.into();
333 assert_eq!(proto.namespace, ProtoContainerdNamespace::NsCri as i32);
334 }
335
336 #[test]
337 fn test_image_info_size_human() {
338 let info = ImageInfo {
339 node: None,
340 name: "test".to_string(),
341 digest: "sha256:abc".to_string(),
342 size: 500,
343 created_at: None,
344 };
345 assert_eq!(info.size_human(), "500 B");
346
347 let info = ImageInfo {
348 size: 2048,
349 ..info.clone()
350 };
351 assert_eq!(info.size_human(), "2.0 KB");
352
353 let info = ImageInfo {
354 size: 52_428_800,
355 ..info.clone()
356 };
357 assert_eq!(info.size_human(), "50.0 MB");
358
359 let info = ImageInfo {
360 size: 2_147_483_648,
361 ..info
362 };
363 assert_eq!(info.size_human(), "2.00 GB");
364 }
365
366 #[test]
367 fn test_image_info_repository_and_tag() {
368 let info = ImageInfo {
370 node: None,
371 name: "docker.io/library/nginx:1.25".to_string(),
372 digest: "sha256:abc".to_string(),
373 size: 0,
374 created_at: None,
375 };
376 assert_eq!(info.repository(), "docker.io/library/nginx");
377 assert_eq!(info.tag(), Some("1.25"));
378 assert!(!info.is_digest_reference());
379
380 let info = ImageInfo {
382 name: "ghcr.io/siderolabs/kubelet@sha256:abc123".to_string(),
383 ..info.clone()
384 };
385 assert_eq!(info.repository(), "ghcr.io/siderolabs/kubelet");
386 assert_eq!(info.tag(), None);
387 assert!(info.is_digest_reference());
388
389 let info = ImageInfo {
391 name: "nginx".to_string(),
392 ..info
393 };
394 assert_eq!(info.repository(), "nginx");
395 assert_eq!(info.tag(), None);
396 }
397
398 #[test]
399 fn test_image_pull_request_builder() {
400 let req = ImagePullRequest::new("nginx:latest").for_cri();
401 assert_eq!(req.reference, "nginx:latest");
402 assert_eq!(req.namespace, ContainerdNamespace::Cri);
403
404 let req = ImagePullRequest::new("alpine:3.18").with_namespace(ContainerdNamespace::Cri);
405 assert_eq!(req.namespace, ContainerdNamespace::Cri);
406 }
407
408 #[test]
409 fn test_image_pull_request_to_proto() {
410 let req = ImagePullRequest::new("ghcr.io/test/image:v1").for_cri();
411 let proto: ProtoImagePullRequest = req.into();
412 assert_eq!(proto.reference, "ghcr.io/test/image:v1");
413 assert_eq!(proto.namespace, ProtoContainerdNamespace::NsCri as i32);
414 }
415
416 #[test]
417 fn test_image_pull_response_nodes() {
418 let response = ImagePullResponse {
419 results: vec![
420 ImagePullResult {
421 node: Some("node1".to_string()),
422 },
423 ImagePullResult {
424 node: Some("node2".to_string()),
425 },
426 ImagePullResult { node: None },
427 ],
428 };
429 assert!(response.all_succeeded());
430 assert_eq!(response.nodes(), vec!["node1", "node2"]);
431 }
432}