1use serde_json::{json, Map, Value};
13
14pub const MIN_API_VERSION: &str = "1.24";
17
18#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum Route {
21 Ping,
23 Version,
25 Info,
27 ContainerList,
29 ContainerInspect(String),
31 ImageList,
33 NetworkList,
35 VolumeList,
37 Mutating,
39 NotFound,
41}
42
43impl Route {
44 pub fn resource(&self) -> Option<&'static str> {
46 match self {
47 Route::Version => Some("version"),
48 Route::Info => Some("info"),
49 Route::ContainerList => Some("containers"),
50 Route::ContainerInspect(_) => Some("container"),
51 Route::ImageList => Some("images"),
52 Route::NetworkList => Some("networks"),
53 Route::VolumeList => Some("volumes"),
54 Route::Ping | Route::Mutating | Route::NotFound => None,
55 }
56 }
57
58 pub fn answering_action(&self) -> Option<&'static str> {
60 match self {
61 Route::Version => Some("send_docker_version"),
62 Route::Info => Some("send_docker_info"),
63 Route::ContainerList => Some("send_docker_containers"),
64 Route::ContainerInspect(_) => Some("send_docker_container"),
65 Route::ImageList => Some("send_docker_images"),
66 Route::NetworkList => Some("send_docker_networks"),
67 Route::VolumeList => Some("send_docker_volumes"),
68 Route::Ping | Route::Mutating | Route::NotFound => None,
69 }
70 }
71
72 pub fn answer_with(&self, query: &Value) -> Option<String> {
83 let all = query.get("all").and_then(Value::as_str) == Some("1");
84 Some(match self {
85 Route::Version => "send_docker_version".to_string(),
86 Route::Info => "send_docker_info".to_string(),
87 Route::ContainerList if all => "send_docker_containers with a containers array \
88 holding every container, running or not, each with its names, image and state \
89 (a stopped container has state exited and its exit_code)"
90 .to_string(),
91 Route::ContainerList => "send_docker_containers with a containers array holding \
92 the running containers, each with its names, image and state"
93 .to_string(),
94 Route::ContainerInspect(id) => format!(
95 "send_docker_container with the fields of the container named {id} (or whose \
96 ID starts with {id}); if there is no such container, send_docker_error with \
97 status 404 and message \"No such container: {id}\""
98 ),
99 Route::ImageList => "send_docker_images with an images array".to_string(),
100 Route::NetworkList => "send_docker_networks with a networks array".to_string(),
101 Route::VolumeList => "send_docker_volumes with a volumes array".to_string(),
102 Route::Ping | Route::Mutating | Route::NotFound => return None,
103 })
104 }
105}
106
107pub fn split_version(path: &str) -> (Option<String>, String) {
113 let trimmed = path.trim_start_matches('/');
114 let (first, rest) = match trimmed.split_once('/') {
115 Some((f, r)) => (f, format!("/{r}")),
116 None => (trimmed, "/".to_string()),
117 };
118 if let Some(v) = first.strip_prefix('v') {
119 if parse_version(v).is_some() {
120 return (Some(v.to_string()), rest);
121 }
122 }
123 (None, path.to_string())
124}
125
126pub fn parse_version(v: &str) -> Option<(u32, u32)> {
128 let (major, minor) = v.split_once('.')?;
129 if major.is_empty() || minor.is_empty() || major.len() > 3 || minor.len() > 3 {
130 return None;
131 }
132 Some((major.parse().ok()?, minor.parse().ok()?))
133}
134
135pub fn resolve(method: &str, path: &str) -> Route {
137 let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
138 let is_read = method == "GET" || method == "HEAD";
139 match segments.as_slice() {
140 ["_ping"] if is_read => Route::Ping,
141 _ if !is_read => Route::Mutating,
142 ["version"] => Route::Version,
143 ["info"] => Route::Info,
144 ["containers", "json"] => Route::ContainerList,
145 ["containers", id, "json"] if !id.is_empty() => Route::ContainerInspect(id.to_string()),
146 ["images", "json"] => Route::ImageList,
147 ["networks"] => Route::NetworkList,
148 ["volumes"] => Route::VolumeList,
149 _ => Route::NotFound,
150 }
151}
152
153pub fn error_body(message: &str) -> Value {
155 json!({ "message": message })
156}
157
158fn field<'a>(obj: &'a Value, names: &[&str]) -> Option<&'a Value> {
164 names
165 .iter()
166 .find_map(|n| obj.get(*n))
167 .filter(|v| !v.is_null())
168}
169
170fn str_field(obj: &Value, names: &[&str]) -> Option<String> {
171 field(obj, names).and_then(|v| match v {
172 Value::String(s) => Some(s.clone()),
173 Value::Number(n) => Some(n.to_string()),
174 _ => None,
175 })
176}
177
178fn u64_field(obj: &Value, names: &[&str], what: &str) -> Result<Option<u64>, String> {
179 match field(obj, names) {
180 None => Ok(None),
181 Some(Value::Number(n)) => n
182 .as_u64()
183 .map(Some)
184 .ok_or_else(|| format!("{what} must be a non-negative integer")),
185 Some(Value::String(s)) => s
186 .trim()
187 .parse::<u64>()
188 .map(Some)
189 .map_err(|_| format!("{what} must be a non-negative integer, got {s:?}")),
190 Some(_) => Err(format!("{what} must be a number")),
191 }
192}
193
194fn string_map(obj: &Value, names: &[&str], what: &str) -> Result<Map<String, Value>, String> {
195 match field(obj, names) {
196 None => Ok(Map::new()),
197 Some(Value::Object(m)) => {
198 let mut out = Map::new();
199 for (k, v) in m {
200 let text = match v {
201 Value::String(s) => s.clone(),
202 Value::Null => String::new(),
203 other => other.to_string(),
204 };
205 out.insert(k.clone(), Value::String(text));
206 }
207 Ok(out)
208 }
209 Some(_) => Err(format!("{what} must be an object of strings")),
210 }
211}
212
213fn string_list(obj: &Value, names: &[&str]) -> Vec<String> {
214 match field(obj, names) {
215 Some(Value::Array(a)) => a
216 .iter()
217 .filter_map(|v| v.as_str().map(str::to_string))
218 .collect(),
219 Some(Value::String(s)) => vec![s.clone()],
220 _ => Vec::new(),
221 }
222}
223
224pub fn derived_id(seed: &str) -> String {
229 use std::hash::{Hash, Hasher};
230 let mut out = String::with_capacity(64);
231 for round in 0u8..4 {
232 let mut h = std::collections::hash_map::DefaultHasher::new();
233 (round, seed).hash(&mut h);
234 out.push_str(&format!("{:016x}", h.finish()));
235 }
236 out
237}
238
239fn valid_id(id: &str) -> bool {
240 !id.is_empty() && id.len() <= 128 && id.chars().all(|c| c.is_ascii_alphanumeric())
241}
242
243fn id_from(given: &str, kind: &str, what: &str) -> Result<String, String> {
252 let bare = given.trim_start_matches("sha256:");
253 if valid_id(bare) {
254 Ok(bare.to_string())
255 } else if valid_name(bare) {
256 Ok(derived_id(&format!("{kind}:{bare}")))
257 } else {
258 Err(format!(
259 "{what}: id {given:?} must be letters and digits (or omit it and NetGet derives one)"
260 ))
261 }
262}
263
264fn valid_name(name: &str) -> bool {
266 let mut chars = name.chars();
267 matches!(chars.next(), Some(c) if c.is_ascii_alphanumeric())
268 && chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '-')
269 && name.len() <= 255
270}
271
272fn now_unix() -> i64 {
273 crate::utils::clock::SystemTime::now()
274 .duration_since(crate::utils::clock::UNIX_EPOCH)
275 .map(|d| d.as_secs() as i64)
276 .unwrap_or(0)
277}
278
279fn created_unix(obj: &Value, what: &str) -> Result<i64, String> {
281 match field(obj, &["created", "Created", "created_at", "CreatedAt"]) {
282 None => Ok(now_unix()),
283 Some(Value::Number(n)) => n
284 .as_i64()
285 .ok_or_else(|| format!("{what} created must be unix seconds or RFC 3339")),
286 Some(Value::String(s)) => chrono::DateTime::parse_from_rfc3339(s.trim())
287 .map(|t| t.timestamp())
288 .or_else(|_| s.trim().parse::<i64>())
289 .map_err(|_| format!("{what} created {s:?} is neither unix seconds nor RFC 3339")),
290 Some(_) => Err(format!("{what} created must be unix seconds or RFC 3339")),
291 }
292}
293
294fn rfc3339(unix: i64) -> String {
295 chrono::DateTime::from_timestamp(unix, 0)
296 .map(|t| t.to_rfc3339_opts(chrono::SecondsFormat::Nanos, true))
297 .unwrap_or_else(|| "0001-01-01T00:00:00Z".to_string())
298}
299
300const STATES: &[&str] = &[
301 "created",
302 "running",
303 "paused",
304 "restarting",
305 "removing",
306 "exited",
307 "dead",
308];
309
310fn state_of(obj: &Value, what: &str) -> Result<String, String> {
311 let raw = match field(obj, &["state", "State"]) {
312 None => "running".to_string(),
313 Some(Value::String(s)) => s.to_ascii_lowercase(),
314 Some(Value::Object(o)) => o
316 .get("Status")
317 .or_else(|| o.get("status"))
318 .and_then(Value::as_str)
319 .unwrap_or("running")
320 .to_ascii_lowercase(),
321 Some(_) => return Err(format!("{what} state must be a string")),
322 };
323 let raw = if raw == "stopped" {
326 "exited".to_string()
327 } else {
328 raw
329 };
330 if STATES.contains(&raw.as_str()) {
331 Ok(raw)
332 } else {
333 Err(format!(
334 "{what} state {raw:?} is not one of {}",
335 STATES.join(", ")
336 ))
337 }
338}
339
340fn default_status(state: &str, exit_code: i64) -> String {
341 match state {
342 "running" => "Up".to_string(),
343 "paused" => "Up (Paused)".to_string(),
344 "restarting" => "Restarting".to_string(),
345 "exited" => format!("Exited ({exit_code})"),
346 "created" => "Created".to_string(),
347 "removing" => "Removal In Progress".to_string(),
348 _ => "Dead".to_string(),
349 }
350}
351
352fn status_of(given: Option<String>, state: &str, exit_code: i64) -> String {
357 let Some(given) = given.filter(|s| !s.trim().is_empty()) else {
358 return default_status(state, exit_code);
359 };
360 if state != "exited" || given.starts_with("Exited (") {
361 return given;
362 }
363 let lower = given.to_ascii_lowercase();
364 let ago = lower.find(" ago").and_then(|end| {
365 let words: Vec<&str> = given[..end].split_whitespace().collect();
366 (words.len() >= 2)
368 .then(|| format!("{} {} ago", words[words.len() - 2], words[words.len() - 1]))
369 });
370 match ago {
371 Some(ago) => format!("Exited ({exit_code}) {ago}"),
372 None => default_status(state, exit_code),
373 }
374}
375
376fn ports(obj: &Value, what: &str) -> Result<Vec<Value>, String> {
378 let Some(list) = field(obj, &["ports", "Ports"]) else {
379 return Ok(Vec::new());
380 };
381 let Value::Array(list) = list else {
382 return Err(format!("{what} ports must be an array"));
383 };
384 let mut out = Vec::with_capacity(list.len());
385 for p in list {
386 let private = u64_field(
387 p,
388 &["private_port", "PrivatePort", "container_port"],
389 "private_port",
390 )?
391 .ok_or_else(|| format!("{what} port entry needs a private_port"))?;
392 if !(1..=65535).contains(&private) {
393 return Err(format!("{what} private_port {private} is not a port"));
394 }
395 let kind = str_field(p, &["type", "Type", "protocol"])
396 .unwrap_or_else(|| "tcp".to_string())
397 .to_ascii_lowercase();
398 if !matches!(kind.as_str(), "tcp" | "udp" | "sctp") {
399 return Err(format!("{what} port type {kind:?} is not tcp, udp or sctp"));
400 }
401 let mut entry = json!({"PrivatePort": private, "Type": kind});
402 if let Some(public) = u64_field(
403 p,
404 &["public_port", "PublicPort", "host_port"],
405 "public_port",
406 )? {
407 if !(1..=65535).contains(&public) {
408 return Err(format!("{what} public_port {public} is not a port"));
409 }
410 entry["PublicPort"] = json!(public);
411 entry["IP"] = json!(
412 str_field(p, &["ip", "IP", "host_ip"]).unwrap_or_else(|| "0.0.0.0".to_string())
413 );
414 }
415 out.push(entry);
416 }
417 Ok(out)
418}
419
420fn command_of(obj: &Value) -> (String, String, Vec<String>) {
422 match field(obj, &["command", "Command", "cmd", "Cmd"]) {
423 Some(Value::Array(parts)) => {
424 let parts: Vec<String> = parts
425 .iter()
426 .filter_map(|p| p.as_str().map(str::to_string))
427 .collect();
428 let path = parts.first().cloned().unwrap_or_default();
429 (parts.join(" "), path, parts.into_iter().skip(1).collect())
430 }
431 Some(Value::String(s)) => {
432 let mut words = s.split_whitespace().map(str::to_string);
433 let path = words.next().unwrap_or_default();
434 (s.clone(), path, words.collect())
435 }
436 _ => (String::new(), String::new(), Vec::new()),
437 }
438}
439
440struct Container {
441 id: String,
442 name: String,
443 image: String,
444 image_id: String,
445 command: String,
446 path: String,
447 args: Vec<String>,
448 state: String,
449 status: String,
450 exit_code: i64,
451 created: i64,
452 ports: Vec<Value>,
453 labels: Map<String, Value>,
454 env: Vec<String>,
455 ip: String,
456}
457
458fn container(obj: &Value, index: usize) -> Result<Container, String> {
459 if !obj.is_object() {
460 return Err(format!("container {index} must be an object"));
461 }
462 let names = string_list(obj, &["names", "Names", "name", "Name"]);
463 let name = names
464 .first()
465 .map(|n| n.trim_start_matches('/').to_string())
466 .unwrap_or_default();
467 let what = if name.is_empty() {
468 format!("container {index}")
469 } else {
470 format!("container '{name}'")
471 };
472 if name.is_empty() {
473 return Err(format!("{what} needs a name"));
474 }
475 if !valid_name(&name) {
476 return Err(format!(
477 "{what}: name must match [a-zA-Z0-9][a-zA-Z0-9_.-]* as Docker requires"
478 ));
479 }
480 let id = match str_field(obj, &["id", "Id", "ID"]) {
481 Some(id) => id_from(&id, "container", &what)?,
482 None => derived_id(&format!("container:{name}")),
483 };
484 let image = str_field(obj, &["image", "Image"]).unwrap_or_default();
485 if image.is_empty() {
486 return Err(format!("{what} needs an image"));
487 }
488 let state = state_of(obj, &what)?;
489 let exit_code = field(obj, &["exit_code", "ExitCode"])
490 .and_then(Value::as_i64)
491 .unwrap_or(0);
492 let status = status_of(str_field(obj, &["status", "Status"]), &state, exit_code);
493 let (command, path, args) = command_of(obj);
494 let env = string_list(obj, &["env", "Env"]);
495 Ok(Container {
496 image_id: format!("sha256:{}", derived_id(&format!("image:{image}"))),
497 id,
498 name,
499 image,
500 command,
501 path,
502 args,
503 state,
504 status,
505 exit_code,
506 created: created_unix(obj, &what)?,
507 ports: ports(obj, &what)?,
508 labels: string_map(obj, &["labels", "Labels"], "labels")?,
509 env,
510 ip: str_field(obj, &["ip", "ip_address", "IPAddress"]).unwrap_or_default(),
511 })
512}
513
514pub fn render_container_list(containers: &Value) -> Result<Value, String> {
516 let list = containers
517 .as_array()
518 .ok_or("'containers' must be an array")?;
519 let mut out = Vec::with_capacity(list.len());
520 let mut seen_short_ids: Vec<String> = Vec::with_capacity(list.len());
521 for (i, c) in list.iter().enumerate() {
522 let mut c = container(c, i)?;
523 let short: String = c.id.chars().take(12).collect();
529 if seen_short_ids.contains(&short) {
530 c.id = derived_id(&format!("container:{}", c.name));
531 }
532 seen_short_ids.push(c.id.chars().take(12).collect());
533 out.push(json!({
534 "Id": c.id,
535 "Names": [format!("/{}", c.name)],
536 "Image": c.image,
537 "ImageID": c.image_id,
538 "Command": c.command,
539 "Created": c.created,
540 "Ports": c.ports,
541 "Labels": c.labels,
542 "State": c.state,
543 "Status": c.status,
544 "HostConfig": {"NetworkMode": "bridge"},
545 "NetworkSettings": {"Networks": {}},
546 "Mounts": [],
547 }));
548 }
549 Ok(Value::Array(out))
550}
551
552pub fn render_container(container_value: &Value) -> Result<Value, String> {
554 let c = container(container_value, 0)?;
555 let running = c.state == "running" || c.state == "paused";
556 let started = if c.state == "created" {
557 "0001-01-01T00:00:00Z".to_string()
558 } else {
559 rfc3339(c.created)
560 };
561 let mut exposed = Map::new();
562 let mut bindings = Map::new();
563 for p in &c.ports {
564 let key = format!(
565 "{}/{}",
566 p["PrivatePort"],
567 p["Type"].as_str().unwrap_or("tcp")
568 );
569 exposed.insert(key.clone(), json!({}));
570 let binding = match p.get("PublicPort") {
571 Some(public) => json!([{"HostIp": p["IP"], "HostPort": public.to_string()}]),
572 None => Value::Null,
573 };
574 bindings.insert(key, binding);
575 }
576 Ok(json!({
577 "Id": c.id,
578 "Created": rfc3339(c.created),
579 "Path": c.path,
580 "Args": c.args,
581 "State": {
582 "Status": c.state,
583 "Running": running,
584 "Paused": c.state == "paused",
585 "Restarting": c.state == "restarting",
586 "OOMKilled": false,
587 "Dead": c.state == "dead",
588 "Pid": if running { 4242 } else { 0 },
589 "ExitCode": c.exit_code,
590 "Error": "",
591 "StartedAt": started,
592 "FinishedAt": if running || c.state == "created" {
593 "0001-01-01T00:00:00Z".to_string()
594 } else {
595 rfc3339(c.created)
596 },
597 },
598 "Image": c.image_id,
599 "Name": format!("/{}", c.name),
600 "RestartCount": 0,
601 "Driver": "overlay2",
602 "Platform": "linux",
603 "MountLabel": "",
604 "ProcessLabel": "",
605 "AppArmorProfile": "",
606 "HostConfig": {"NetworkMode": "bridge", "RestartPolicy": {"Name": "no", "MaximumRetryCount": 0}},
607 "Mounts": [],
608 "Config": {
609 "Hostname": c.id.chars().take(12).collect::<String>(),
610 "Image": c.image,
611 "Cmd": std::iter::once(c.path.clone()).chain(c.args.iter().cloned())
612 .filter(|s| !s.is_empty()).collect::<Vec<_>>(),
613 "Env": c.env,
614 "Labels": c.labels,
615 "ExposedPorts": exposed,
616 "Tty": false,
617 "OpenStdin": false,
618 },
619 "NetworkSettings": {
620 "Ports": bindings,
621 "IPAddress": c.ip,
622 "Networks": {},
623 },
624 }))
625}
626
627pub fn render_images(images: &Value) -> Result<Value, String> {
629 let list = images.as_array().ok_or("'images' must be an array")?;
630 let mut out = Vec::with_capacity(list.len());
631 for (i, img) in list.iter().enumerate() {
632 if !img.is_object() {
633 return Err(format!("image {i} must be an object"));
634 }
635 let tags = string_list(img, &["repo_tags", "RepoTags", "tags", "tag"]);
636 for t in &tags {
637 let reference_char = |c: char| c.is_ascii_alphanumeric() || "._-:/@".contains(c);
640 if t.is_empty() || t.len() > 255 || !t.chars().all(reference_char) {
641 return Err(format!("image {i}: tag {t:?} is not a valid reference"));
642 }
643 }
644 let seed = tags
645 .first()
646 .cloned()
647 .unwrap_or_else(|| format!("image-{i}"));
648 let id = match str_field(img, &["id", "Id", "ID"]) {
649 Some(id) => format!("sha256:{}", id_from(&id, "image", &format!("image {i}"))?),
650 None => format!("sha256:{}", derived_id(&format!("image:{seed}"))),
651 };
652 let size = u64_field(img, &["size", "Size"], "size")?.unwrap_or(0);
653 out.push(json!({
654 "Id": id,
655 "ParentId": "",
656 "RepoTags": tags,
657 "RepoDigests": string_list(img, &["repo_digests", "RepoDigests"]),
658 "Created": created_unix(img, &format!("image {i}"))?,
659 "Size": size,
660 "SharedSize": -1,
661 "Labels": string_map(img, &["labels", "Labels"], "labels")?,
662 "Containers": -1,
663 }));
664 }
665 Ok(Value::Array(out))
666}
667
668pub fn render_networks(networks: &Value) -> Result<Value, String> {
670 let list = networks.as_array().ok_or("'networks' must be an array")?;
671 let mut out = Vec::with_capacity(list.len());
672 for (i, n) in list.iter().enumerate() {
673 let name = str_field(n, &["name", "Name"])
674 .filter(|s| valid_name(s))
675 .ok_or_else(|| format!("network {i} needs a valid name"))?;
676 let id = str_field(n, &["id", "Id", "ID"])
677 .filter(|s| valid_id(s))
678 .unwrap_or_else(|| derived_id(&format!("network:{name}")));
679 out.push(json!({
680 "Name": name,
681 "Id": id,
682 "Created": rfc3339(created_unix(n, &format!("network '{name}'"))?),
683 "Scope": str_field(n, &["scope", "Scope"]).unwrap_or_else(|| "local".to_string()),
684 "Driver": str_field(n, &["driver", "Driver"]).unwrap_or_else(|| "bridge".to_string()),
685 "EnableIPv6": false,
686 "IPAM": {"Driver": "default", "Options": null, "Config": []},
687 "Internal": field(n, &["internal", "Internal"]).and_then(Value::as_bool).unwrap_or(false),
688 "Attachable": false,
689 "Ingress": false,
690 "ConfigFrom": {"Network": ""},
691 "ConfigOnly": false,
692 "Containers": {},
693 "Options": {},
694 "Labels": string_map(n, &["labels", "Labels"], "labels")?,
695 }));
696 }
697 Ok(Value::Array(out))
698}
699
700pub fn render_volumes(volumes: &Value) -> Result<Value, String> {
702 let list = volumes.as_array().ok_or("'volumes' must be an array")?;
703 let mut out = Vec::with_capacity(list.len());
704 for (i, v) in list.iter().enumerate() {
705 let name = str_field(v, &["name", "Name"])
706 .filter(|s| valid_name(s))
707 .ok_or_else(|| format!("volume {i} needs a valid name"))?;
708 out.push(json!({
709 "Name": name,
710 "Driver": str_field(v, &["driver", "Driver"]).unwrap_or_else(|| "local".to_string()),
711 "Mountpoint": format!("/var/lib/docker/volumes/{name}/_data"),
712 "CreatedAt": rfc3339(created_unix(v, &format!("volume '{name}'"))?),
713 "Labels": string_map(v, &["labels", "Labels"], "labels")?,
714 "Scope": "local",
715 "Options": {},
716 }));
717 }
718 Ok(json!({"Volumes": out, "Warnings": []}))
719}
720
721#[derive(Clone, Debug)]
723pub struct EngineIdentity {
724 pub engine_version: String,
725 pub api_version: String,
726}
727
728fn text_or(obj: &Value, names: &[&str], default: &str) -> String {
729 str_field(obj, names).unwrap_or_else(|| default.to_string())
730}
731
732pub fn render_version(data: &Value, id: &EngineIdentity) -> Result<Value, String> {
734 let version = text_or(data, &["version", "Version"], &id.engine_version);
735 let api = text_or(data, &["api_version", "ApiVersion"], &id.api_version);
736 if parse_version(&api).is_none() {
737 return Err(format!("api_version {api:?} must look like 1.47"));
738 }
739 let os = text_or(data, &["os", "Os"], "linux");
740 let arch = text_or(data, &["arch", "Arch"], "amd64");
741 let kernel = text_or(data, &["kernel_version", "KernelVersion"], "6.8.0-netget");
742 let go = text_or(data, &["go_version", "GoVersion"], "go1.22.10");
743 let commit = text_or(data, &["git_commit", "GitCommit"], "netget");
744 let built = text_or(
745 data,
746 &["build_time", "BuildTime"],
747 "2025-01-01T00:00:00.000000000+00:00",
748 );
749 Ok(json!({
750 "Platform": {"Name": text_or(data, &["platform", "Platform"], "Docker Engine - Community")},
751 "Components": [{
752 "Name": "Engine",
753 "Version": version,
754 "Details": {
755 "ApiVersion": api,
756 "Arch": arch,
757 "BuildTime": built,
758 "Experimental": "false",
759 "GitCommit": commit,
760 "GoVersion": go,
761 "KernelVersion": kernel,
762 "MinAPIVersion": MIN_API_VERSION,
763 "Os": os,
764 }
765 }],
766 "Version": version,
767 "ApiVersion": api,
768 "MinAPIVersion": MIN_API_VERSION,
769 "GitCommit": commit,
770 "GoVersion": go,
771 "Os": os,
772 "Arch": arch,
773 "KernelVersion": kernel,
774 "BuildTime": built,
775 }))
776}
777
778pub fn render_info(data: &Value, id: &EngineIdentity) -> Result<Value, String> {
780 let count = |names: &[&str], what: &str| -> Result<u64, String> {
781 Ok(u64_field(data, names, what)?.unwrap_or(0))
782 };
783 let running = count(
784 &["containers_running", "ContainersRunning"],
785 "containers_running",
786 )?;
787 let paused = count(
788 &["containers_paused", "ContainersPaused"],
789 "containers_paused",
790 )?;
791 let stopped = count(
792 &["containers_stopped", "ContainersStopped"],
793 "containers_stopped",
794 )?;
795 let total = u64_field(data, &["containers", "Containers"], "containers")?
796 .unwrap_or(running + paused + stopped);
797 let name = text_or(data, &["name", "Name"], "netget");
798 let mut info = json!({
800 "ID": text_or(data, &["id", "ID"], &derived_id(&format!("engine:{name}"))[..36]),
801 "Containers": total,
802 "ContainersRunning": running,
803 "ContainersPaused": paused,
804 "ContainersStopped": stopped,
805 "Images": count(&["images", "Images"], "images")?,
806 "Driver": text_or(data, &["driver", "Driver"], "overlay2"),
807 "DriverStatus": [["Backing Filesystem", "extfs"]],
808 "Plugins": {
809 "Volume": ["local"],
810 "Network": ["bridge", "host", "null"],
811 "Authorization": null,
812 "Log": ["json-file", "local"],
813 },
814 "MemoryLimit": true,
815 "SwapLimit": true,
816 "CpuCfsPeriod": true,
817 "CpuCfsQuota": true,
818 "CPUShares": true,
819 "CPUSet": true,
820 "PidsLimit": true,
821 "IPv4Forwarding": true,
822 "Debug": false,
823 "NFd": 30,
824 "OomKillDisable": false,
825 "NGoroutines": 40,
826 "SystemTime": rfc3339(now_unix()),
827 "LoggingDriver": "json-file",
828 "CgroupDriver": "systemd",
829 "CgroupVersion": "2",
830 "NEventsListener": 0,
831 });
832 let rest = json!({
833 "KernelVersion": text_or(data, &["kernel_version", "KernelVersion"], "6.8.0-netget"),
834 "OperatingSystem": text_or(data, &["operating_system", "OperatingSystem"], "NetGet Linux"),
835 "OSVersion": text_or(data, &["os_version", "OSVersion"], ""),
836 "OSType": text_or(data, &["os_type", "OSType"], "linux"),
837 "Architecture": text_or(data, &["architecture", "Architecture"], "x86_64"),
838 "IndexServerAddress": "https://index.docker.io/v1/",
839 "RegistryConfig": {
840 "IndexConfigs": {},
841 "InsecureRegistryCIDRs": ["127.0.0.0/8"],
842 "Mirrors": [],
843 },
844 "NCPU": u64_field(data, &["ncpu", "NCPU", "cpus"], "ncpu")?.unwrap_or(4),
845 "MemTotal": u64_field(data, &["mem_total", "MemTotal", "memory_bytes"], "mem_total")?
846 .unwrap_or(8 * 1024 * 1024 * 1024),
847 "DockerRootDir": "/var/lib/docker",
848 "Name": name,
849 "Labels": string_list(data, &["labels", "Labels"]),
850 "ExperimentalBuild": false,
851 "ServerVersion": text_or(data, &["server_version", "ServerVersion", "version"], &id.engine_version),
852 "Runtimes": {"runc": {"path": "runc"}},
853 "DefaultRuntime": "runc",
854 "Swarm": {"NodeID": "", "NodeAddr": "", "LocalNodeState": "inactive", "ControlAvailable": false, "Error": "", "RemoteManagers": null},
855 "LiveRestoreEnabled": false,
856 "Isolation": "",
857 "InitBinary": "docker-init",
858 "SecurityOptions": ["name=seccomp,profile=builtin", "name=cgroupns"],
859 "Warnings": [],
860 });
861 if let (Some(a), Value::Object(b)) = (info.as_object_mut(), rest) {
862 a.extend(b);
863 }
864 Ok(info)
865}