gatehouse_runtime_protocol/lifecycle_v2/
native_create.rs1use 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 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 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 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}