Skip to main content

netget/client/docker/
schema.rs

1//! Selected read-only Engine response fields, with their native JSON types.
2use anyhow::{ensure, Context, Result};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::BTreeMap;
6pub const MAX_BODY: usize = 4 * 1024 * 1024;
7pub const MAX_ITEMS: usize = 4096;
8pub const MAX_TEXT: usize = 16 * 1024;
9type Strings = BTreeMap<String, String>;
10macro_rules! object {
11    ($name:ident { $($field:ident : $ty:ty  =>  $wire:literal),* $(,)? })  =>  {
12        #[derive(Debug, Deserialize, Serialize)]
13        pub struct $name { $(#[serde(rename(deserialize = $wire))] pub $field: $ty),* }
14    }
15}
16object!(Component { name:String => "Name", version:String => "Version", details:Option<Strings> => "Details" });
17object!(Platform { name:String => "Name" });
18object!(Version {
19    version:String => "Version", api_version:String => "ApiVersion", min_api_version:Option<String> => "MinAPIVersion",
20    os:String => "Os", arch:String => "Arch", git_commit:Option<String> => "GitCommit", go_version:Option<String> => "GoVersion",
21    kernel_version:Option<String> => "KernelVersion", experimental:Option<bool> => "Experimental",
22    components:Option<Vec<Component>> => "Components", platform:Option<Platform> => "Platform"
23});
24object!(Info {
25    id:String => "ID", name:String => "Name", server_version:String => "ServerVersion",
26    containers:u64 => "Containers", containers_running:u64 => "ContainersRunning", containers_paused:u64 => "ContainersPaused",
27    containers_stopped:u64 => "ContainersStopped", images:u64 => "Images", ncpu:u64 => "NCPU", memory_total:u64 => "MemTotal",
28    driver:Option<String> => "Driver", operating_system:Option<String> => "OperatingSystem", os_type:Option<String> => "OSType",
29    architecture:Option<String> => "Architecture", kernel_version:Option<String> => "KernelVersion",
30    labels:Option<Vec<String>> => "Labels", warnings:Option<Vec<String>> => "Warnings"
31});
32object!(Port { private_port:u16 => "PrivatePort", public_port:Option<u16> => "PublicPort", protocol:String => "Type", ip:Option<String> => "IP" });
33object!(Container {
34    id:String => "Id", names:Vec<String> => "Names", image:String => "Image", image_id:String => "ImageID", command:String => "Command",
35    created:i64 => "Created", ports:Option<Vec<Port>> => "Ports", labels:Option<Strings> => "Labels", state:String => "State", status:String => "Status",
36    size_rw:Option<u64> => "SizeRw", size_root_fs:Option<u64> => "SizeRootFs"
37});
38object!(ContainerState {
39    status:String => "Status", running:bool => "Running", paused:bool => "Paused", restarting:bool => "Restarting",
40    oom_killed:bool => "OOMKilled", dead:bool => "Dead", pid:u64 => "Pid", exit_code:i64 => "ExitCode", error:String => "Error",
41    started_at:String => "StartedAt", finished_at:String => "FinishedAt"
42});
43object!(Config {
44    image:String => "Image", hostname:Option<String> => "Hostname", user:Option<String> => "User", env:Option<Vec<String>> => "Env",
45    cmd:Option<Vec<String>> => "Cmd", entrypoint:Option<Vec<String>> => "Entrypoint", working_dir:Option<String> => "WorkingDir",
46    labels:Option<Strings> => "Labels", tty:Option<bool> => "Tty", open_stdin:Option<bool> => "OpenStdin"
47});
48object!(Binding { host_ip:String => "HostIp", host_port:String => "HostPort" });
49object!(HostConfig {
50    network_mode:Option<String> => "NetworkMode", memory:Option<i64> => "Memory", cpu_shares:Option<i64> => "CpuShares",
51    port_bindings:Option<BTreeMap<String,Option<Vec<Binding>>>> => "PortBindings"
52});
53object!(EndpointSettings {
54    network_id:Option<String> => "NetworkID", endpoint_id:Option<String> => "EndpointID", ip_address:Option<String> => "IPAddress",
55    ip_prefix_len:Option<u8> => "IPPrefixLen", gateway:Option<String> => "Gateway", mac_address:Option<String> => "MacAddress",
56    global_ipv6_address:Option<String> => "GlobalIPv6Address", global_ipv6_prefix_len:Option<u8> => "GlobalIPv6PrefixLen"
57});
58object!(NetworkSettings {
59    ports:Option<BTreeMap<String,Option<Vec<Binding>>>> => "Ports", networks:Option<BTreeMap<String,EndpointSettings>> => "Networks"
60});
61object!(Mount {
62    kind:String => "Type", name:Option<String> => "Name", source:String => "Source", destination:String => "Destination",
63    driver:Option<String> => "Driver", mode:Option<String> => "Mode", read_write:bool => "RW", propagation:Option<String> => "Propagation"
64});
65object!(Inspect {
66    id:String => "Id", name:String => "Name", created:String => "Created", image:String => "Image", path:String => "Path", args:Vec<String> => "Args",
67    state:ContainerState => "State", config:Config => "Config", host_config:HostConfig => "HostConfig", network_settings:NetworkSettings => "NetworkSettings",
68    mounts:Option<Vec<Mount>> => "Mounts", size_rw:Option<u64> => "SizeRw", size_root_fs:Option<u64> => "SizeRootFs"
69});
70object!(Image {
71    id:String => "Id", parent_id:Option<String> => "ParentId", repo_tags:Option<Vec<String>> => "RepoTags", repo_digests:Option<Vec<String>> => "RepoDigests",
72    created:i64 => "Created", size:u64 => "Size", shared_size:Option<i64> => "SharedSize", containers:Option<i64> => "Containers", labels:Option<Strings> => "Labels"
73});
74object!(IpamConfig { subnet:Option<String> => "Subnet", ip_range:Option<String> => "IPRange", gateway:Option<String> => "Gateway", auxiliary_addresses:Option<Strings> => "AuxiliaryAddresses" });
75object!(Ipam { driver:String => "Driver", options:Option<Strings> => "Options", config:Option<Vec<IpamConfig>> => "Config" });
76object!(Network {
77    id:String => "Id", name:String => "Name", created:Option<String> => "Created", scope:String => "Scope", driver:String => "Driver",
78    enable_ipv6:Option<bool> => "EnableIPv6", internal:bool => "Internal", attachable:Option<bool> => "Attachable", ingress:Option<bool> => "Ingress",
79    ipam:Ipam => "IPAM", options:Option<Strings> => "Options", labels:Option<Strings> => "Labels"
80});
81object!(Usage { ref_count:i64 => "RefCount", size:i64 => "Size" });
82object!(Volume {
83    name:String => "Name", driver:String => "Driver", mountpoint:String => "Mountpoint", created_at:Option<String> => "CreatedAt",
84    scope:String => "Scope", labels:Option<Strings> => "Labels", options:Option<Strings> => "Options", usage_data:Option<Usage> => "UsageData"
85});
86object!(Volumes { volumes:Option<Vec<Volume>> => "Volumes", warnings:Option<Vec<String>> => "Warnings" });
87object!(Error { message:String => "message" });
88struct Seed<'a> {
89    depth: usize,
90    nodes: &'a mut usize,
91}
92impl<'de> serde::de::DeserializeSeed<'de> for Seed<'_> {
93    type Value = Value;
94    fn deserialize<D: serde::Deserializer<'de>>(
95        self,
96        d: D,
97    ) -> std::result::Result<Value, D::Error> {
98        use serde::de::Error;
99        *self.nodes += 1;
100        if self.depth > 32 || *self.nodes > 65536 {
101            return Err(D::Error::custom("Docker JSON nesting/node limit"));
102        }
103        d.deserialize_any(self)
104    }
105}
106impl<'de> serde::de::Visitor<'de> for Seed<'_> {
107    type Value = Value;
108    fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
109        f.write_str("bounded Docker JSON")
110    }
111    fn visit_bool<E: serde::de::Error>(self, v: bool) -> std::result::Result<Value, E> {
112        Ok(Value::Bool(v))
113    }
114    fn visit_i64<E: serde::de::Error>(self, v: i64) -> std::result::Result<Value, E> {
115        Ok(v.into())
116    }
117    fn visit_u64<E: serde::de::Error>(self, v: u64) -> std::result::Result<Value, E> {
118        Ok(v.into())
119    }
120    fn visit_f64<E: serde::de::Error>(self, v: f64) -> std::result::Result<Value, E> {
121        serde_json::Number::from_f64(v)
122            .map(Value::Number)
123            .ok_or_else(|| E::custom("invalid JSON number"))
124    }
125    fn visit_unit<E: serde::de::Error>(self) -> std::result::Result<Value, E> {
126        Ok(Value::Null)
127    }
128    fn visit_str<E: serde::de::Error>(self, v: &str) -> std::result::Result<Value, E> {
129        if v.len() > MAX_TEXT {
130            return Err(E::custom("Docker text limit"));
131        }
132        Ok(Value::String(v.into()))
133    }
134    fn visit_string<E: serde::de::Error>(self, v: String) -> std::result::Result<Value, E> {
135        if v.len() > MAX_TEXT {
136            return Err(E::custom("Docker text limit"));
137        }
138        Ok(Value::String(v))
139    }
140    fn visit_seq<A: serde::de::SeqAccess<'de>>(
141        self,
142        mut a: A,
143    ) -> std::result::Result<Value, A::Error> {
144        use serde::de::Error;
145        let mut values = Vec::new();
146        while let Some(v) = a.next_element_seed(Seed {
147            depth: self.depth + 1,
148            nodes: self.nodes,
149        })? {
150            if values.len() >= MAX_ITEMS {
151                return Err(A::Error::custom("Docker array limit"));
152            }
153            values.push(v);
154        }
155        Ok(Value::Array(values))
156    }
157    fn visit_map<A: serde::de::MapAccess<'de>>(
158        self,
159        mut a: A,
160    ) -> std::result::Result<Value, A::Error> {
161        use serde::de::Error;
162        let mut values = serde_json::Map::new();
163        while let Some(key) = a.next_key::<String>()? {
164            if key.len() > 256 || values.len() >= 256 {
165                return Err(A::Error::custom("Docker object field/name limit"));
166            }
167            if values.contains_key(&key) {
168                return Err(A::Error::custom("duplicate Docker JSON field"));
169            }
170            let value = a.next_value_seed(Seed {
171                depth: self.depth + 1,
172                nodes: self.nodes,
173            })?;
174            values.insert(key, value);
175        }
176        Ok(Value::Object(values))
177    }
178}
179pub fn json(body: &[u8]) -> Result<Value> {
180    ensure!(body.len() <= MAX_BODY, "Docker body limit");
181    use serde::de::DeserializeSeed;
182    let mut decoder = serde_json::Deserializer::from_slice(body);
183    let value = Seed {
184        depth: 0,
185        nodes: &mut 0,
186    }
187    .deserialize(&mut decoder)
188    .context("invalid Docker JSON")?;
189    decoder.end().context("trailing Docker JSON content")?;
190    Ok(value)
191}
192fn typed<T: serde::de::DeserializeOwned + Serialize>(value: Value) -> Result<Value> {
193    Ok(serde_json::to_value(
194        serde_json::from_value::<T>(value).context("invalid Docker response schema")?,
195    )?)
196}
197fn state(value: &str) -> Result<()> {
198    ensure!(
199        [
200            "created",
201            "running",
202            "paused",
203            "restarting",
204            "removing",
205            "exited",
206            "dead"
207        ]
208        .contains(&value),
209        "invalid container state"
210    );
211    Ok(())
212}
213fn id(value: &str) -> Result<()> {
214    ensure!(
215        !value.is_empty()
216            && value.len() <= 256
217            && value
218                .bytes()
219                .all(|c| c.is_ascii_alphanumeric() || b"_-.".contains(&c)),
220        "invalid container ID"
221    );
222    Ok(())
223}
224pub fn parse(operation: &str, body: &[u8]) -> Result<Value> {
225    let value = json(body)?;
226    let result = match operation {
227        "version" => typed::<Version>(value)?,
228        "info" => typed::<Info>(value)?,
229        "containers" => typed::<Vec<Container>>(value)?,
230        "container" => typed::<Inspect>(value)?,
231        "images" => typed::<Vec<Image>>(value)?,
232        "networks" => typed::<Vec<Network>>(value)?,
233        "volumes" => typed::<Volumes>(value)?,
234        _ => anyhow::bail!("unknown Docker response operation"),
235    };
236    match operation {
237        "version" => {
238            super::api_version(result["api_version"].as_str().unwrap())?;
239            if let Some(v) = result["min_api_version"].as_str() {
240                ensure!(
241                    super::api_version(v)?
242                        <= super::api_version(result["api_version"].as_str().unwrap())?,
243                    "invalid API version range"
244                );
245            }
246        }
247        "info" => {
248            ensure!(
249                result["containers_running"]
250                    .as_u64()
251                    .unwrap()
252                    .checked_add(result["containers_paused"].as_u64().unwrap())
253                    .and_then(|n| n.checked_add(result["containers_stopped"].as_u64().unwrap()))
254                    == result["containers"].as_u64(),
255                "inconsistent container counts"
256            );
257        }
258        "containers" => {
259            for c in result.as_array().unwrap() {
260                id(c["id"].as_str().unwrap())?;
261                state(c["state"].as_str().unwrap())?;
262                for p in c["ports"].as_array().into_iter().flatten() {
263                    ensure!(
264                        p["private_port"].as_u64().unwrap() > 0
265                            && p["public_port"].as_u64() != Some(0),
266                        "invalid container port"
267                    );
268                    ensure!(
269                        ["tcp", "udp", "sctp"].contains(&p["protocol"].as_str().unwrap()),
270                        "invalid port protocol"
271                    );
272                }
273            }
274        }
275        "container" => {
276            id(result["id"].as_str().unwrap())?;
277            state(result["state"]["status"].as_str().unwrap())?;
278        }
279        "images" => {
280            for i in result.as_array().unwrap() {
281                for field in ["shared_size", "containers"] {
282                    if let Some(n) = i[field].as_i64() {
283                        ensure!(n >= -1, "invalid image {field}");
284                    }
285                }
286            }
287        }
288        "volumes" => {
289            if let Some(volumes) = result["volumes"].as_array() {
290                for v in volumes {
291                    if let Some(usage) = v["usage_data"].as_object() {
292                        for field in ["size", "ref_count"] {
293                            ensure!(
294                                usage[field].as_i64().unwrap() >= -1,
295                                "invalid volume usage {field}"
296                            );
297                        }
298                    }
299                }
300            }
301        }
302        _ => {}
303    }
304    Ok(result)
305}
306pub fn error(body: &[u8]) -> Result<String> {
307    let e: Error = serde_json::from_value(json(body)?).context("invalid Docker error schema")?;
308    Ok(e.message)
309}