Skip to main content

gatehouse_runtime_protocol/lifecycle_v2/
native_create.rs

1//! Restricted native SpecGenerator encoding. No dispatch or durable authority.
2use super::{
3    native_exec::decode_created,
4    ownership::{validate_exposed, validate_plan, ExpectedContainer, FixedPort},
5};
6use serde::Serialize;
7use std::collections::{BTreeMap, BTreeSet};
8type Result<T> = std::result::Result<T, &'static str>;
9pub const CREATE_ROUTE: &str = "/v6.1.4/libpod/containers/create";
10
11pub struct CreatePlan {
12    name: String,
13    image: String,
14    labels: BTreeMap<String, String>,
15    port: Option<FixedPort>,
16    image_exposed_ports: BTreeSet<String>,
17}
18#[derive(Serialize)]
19struct Wire<'a> {
20    name: &'a str,
21    image: &'a str,
22    labels: &'a BTreeMap<String, String>,
23    env: BTreeMap<String, String>,
24    env_host: bool,
25    httpproxy: bool,
26    netns: Namespace,
27    portmappings: Vec<Mapping>,
28    publish_image_ports: bool,
29    privileged: bool,
30    remove: bool,
31    #[serde(rename = "removeImage")]
32    remove_image: bool,
33    restart_policy: &'a str,
34}
35#[derive(Serialize)]
36struct Namespace {
37    nsmode: &'static str,
38}
39#[derive(Serialize)]
40struct Mapping {
41    host_ip: &'static str,
42    container_port: u16,
43    host_port: u16,
44    range: u16,
45    protocol: &'static str,
46}
47impl CreatePlan {
48    /// Image must already be a resolved local full ID. Labels must be the full
49    /// merged image/operator map, derived elsewhere under protected provenance.
50    pub fn new(
51        name: String,
52        image: String,
53        labels: BTreeMap<String, String>,
54        port: Option<FixedPort>,
55    ) -> Result<Self> {
56        validate_plan(&name, &image, &labels, port.as_ref())?;
57        Ok(Self {
58            name,
59            image,
60            labels,
61            port,
62            image_exposed_ports: BTreeSet::new(),
63        })
64    }
65    /// Carry reviewed image metadata without turning it into host mappings.
66    /// Image inspection/provenance must supply this exact set at integration.
67    pub fn with_image_exposed_ports(mut self, ports: BTreeSet<String>) -> Result<Self> {
68        validate_exposed(&ports)?;
69        self.image_exposed_ports = ports;
70        Ok(self)
71    }
72    pub fn body(&self) -> Result<Vec<u8>> {
73        let ports = self
74            .port
75            .as_ref()
76            .map(|p| Mapping {
77                host_ip: "127.0.0.1",
78                container_port: p.container,
79                host_port: p.host,
80                range: 1,
81                protocol: "tcp",
82            })
83            .into_iter()
84            .collect();
85        serde_json::to_vec(&Wire {
86            name: &self.name,
87            image: &self.image,
88            labels: &self.labels,
89            env: BTreeMap::new(),
90            env_host: false,
91            httpproxy: false,
92            netns: Namespace { nsmode: "bridge" },
93            portmappings: ports,
94            publish_image_ports: false,
95            privileged: false,
96            remove: false,
97            remove_image: false,
98            restart_policy: "no",
99        })
100        .map_err(|_| "native create encoding rejected")
101    }
102    /// Only the exact successful response supplies the ID. Failure must be
103    /// retained as unknown by the future journal; never discover or retry by name.
104    pub fn expected_from_response(&self, status: u16, body: &[u8]) -> Result<ExpectedContainer> {
105        let expected = ExpectedContainer {
106            id: decode_created(status, body, true)?,
107            name: self.name.clone(),
108            image_id: self.image.clone(),
109            labels: self.labels.clone(),
110            port: self.port.clone(),
111            image_exposed_ports: self.image_exposed_ports.clone(),
112        };
113        expected.validate()?;
114        Ok(expected)
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use serde_json::json;
122    fn plan(port: Option<FixedPort>) -> CreatePlan {
123        CreatePlan::new(
124            "fixture".into(),
125            "b".repeat(64),
126            BTreeMap::from([("lease".into(), "owned".into())]),
127            port,
128        )
129        .unwrap()
130    }
131    #[test]
132    fn exact_native_body_and_explicit_range() {
133        let p = plan(Some(FixedPort {
134            container: 8080,
135            host: 18080,
136        }));
137        let expected = format!(
138            r#"{{"name":"fixture","image":"{}","labels":{{"lease":"owned"}},"env":{{}},"env_host":false,"httpproxy":false,"netns":{{"nsmode":"bridge"}},"portmappings":[{{"host_ip":"127.0.0.1","container_port":8080,"host_port":18080,"range":1,"protocol":"tcp"}}],"publish_image_ports":false,"privileged":false,"remove":false,"removeImage":false,"restart_policy":"no"}}"#,
139            "b".repeat(64)
140        );
141        assert_eq!(p.body().unwrap(), expected.as_bytes());
142        let v: serde_json::Value = serde_json::from_slice(&plan(None).body().unwrap()).unwrap();
143        assert_eq!(v["portmappings"], json!([]));
144        for absent in ["restart_tries", "expose", "networks", "HostConfig"] {
145            assert!(v.get(absent).is_none());
146        }
147    }
148    #[test]
149    fn response_id_is_only_source_of_expected_container() {
150        let p = plan(None);
151        let body = serde_json::to_vec(&json!({"Id":"a".repeat(64),"Warnings":[]})).unwrap();
152        let expected = p.expected_from_response(201, &body).unwrap();
153        assert_eq!(expected.id, "a".repeat(64));
154        assert_eq!(expected.image_id, "b".repeat(64));
155        assert_eq!(expected.labels["lease"], "owned");
156        for status in [200, 204, 409, 500] {
157            assert!(p.expected_from_response(status, &body).is_err());
158        }
159        assert!(p.expected_from_response(201, b"{}").is_err());
160    }
161    #[test]
162    fn unresolved_image_and_dynamic_port_refused() {
163        assert!(CreatePlan::new(
164            "fixture".into(),
165            "image:latest".into(),
166            BTreeMap::from([("lease".into(), "owned".into())]),
167            None
168        )
169        .is_err());
170        assert!(CreatePlan::new(
171            "fixture".into(),
172            "b".repeat(64),
173            BTreeMap::from([("lease".into(), "owned".into())]),
174            Some(FixedPort {
175                container: 8080,
176                host: 0
177            })
178        )
179        .is_err());
180    }
181    #[test]
182    fn image_expose_metadata_does_not_publish_ports() {
183        let empty_body = plan(None).body().unwrap();
184        let ports = BTreeSet::from(["80/tcp".into(), "53/udp".into()]);
185        let p = plan(None).with_image_exposed_ports(ports.clone()).unwrap();
186        assert_eq!(p.body().unwrap(), empty_body);
187        let expected = p
188            .expected_from_response(
189                201,
190                &serde_json::to_vec(&json!({"Id":"a".repeat(64),"Warnings":[]})).unwrap(),
191            )
192            .unwrap();
193        assert_eq!(expected.image_exposed_ports, ports);
194        assert!(expected.port.is_none());
195        assert!(plan(None)
196            .with_image_exposed_ports(BTreeSet::from(["080/tcp".into()]))
197            .is_err());
198    }
199}