1use std::collections::BTreeMap;
4
5use serde::Serialize;
6
7use super::diagnostic::{pointer_segment, ComposeDiagnostic};
8use super::{
9 ComposeConfig, ComposeDiagnosticCode, ComposeNormalizationError, DependsOn, DependsOnCondition,
10 DnsConfig, EnvVars, HealthcheckConfig, Labels, NetworkDeclaration, ServiceConfig,
11 ServiceNetworkConfig, ServiceNetworks, StringOrList, VolumeDeclaration,
12};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct NormalizedComposeConfig {
20 pub services: BTreeMap<String, NormalizedServiceConfig>,
22 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
24 pub volumes: BTreeMap<String, NormalizedVolumeDeclaration>,
25 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
27 pub networks: BTreeMap<String, NormalizedNetworkDeclaration>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
32pub struct NormalizedServiceConfig {
33 #[serde(skip_serializing_if = "Option::is_none")]
35 pub image: Option<String>,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub entrypoint: Option<Vec<String>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub command: Option<Vec<String>>,
42 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
44 pub environment: BTreeMap<String, String>,
45 #[serde(skip_serializing_if = "Vec::is_empty")]
47 pub env_file: Vec<String>,
48 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
51 pub secret_environment: BTreeMap<String, String>,
52 #[serde(skip_serializing_if = "Vec::is_empty")]
54 pub ports: Vec<String>,
55 #[serde(skip_serializing_if = "Vec::is_empty")]
57 pub volumes: Vec<String>,
58 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
60 pub depends_on: BTreeMap<String, NormalizedDependsOn>,
61 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
63 pub networks: BTreeMap<String, NormalizedServiceNetwork>,
64 #[serde(skip_serializing_if = "Option::is_none")]
66 pub cpus: Option<u32>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub mem_limit: Option<String>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub restart: Option<String>,
73 #[serde(skip_serializing_if = "Vec::is_empty")]
75 pub dns: Vec<String>,
76 #[serde(skip_serializing_if = "Vec::is_empty")]
78 pub tmpfs: Vec<String>,
79 #[serde(skip_serializing_if = "Vec::is_empty")]
81 pub cap_add: Vec<String>,
82 #[serde(skip_serializing_if = "Vec::is_empty")]
84 pub cap_drop: Vec<String>,
85 #[serde(skip_serializing_if = "is_false")]
87 pub privileged: bool,
88 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
90 pub labels: BTreeMap<String, String>,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub healthcheck: Option<NormalizedHealthcheckConfig>,
94 #[serde(skip_serializing_if = "Option::is_none")]
96 pub working_dir: Option<String>,
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub hostname: Option<String>,
100 #[serde(skip_serializing_if = "Vec::is_empty")]
102 pub extra_hosts: Vec<String>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
107pub struct NormalizedDependsOn {
108 pub condition: String,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
114pub struct NormalizedServiceNetwork {
115 #[serde(skip_serializing_if = "Vec::is_empty")]
117 pub aliases: Vec<String>,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
122pub struct NormalizedHealthcheckConfig {
123 #[serde(skip_serializing_if = "Vec::is_empty")]
125 pub test: Vec<String>,
126 #[serde(skip_serializing_if = "is_false")]
128 pub disable: bool,
129 #[serde(skip_serializing_if = "Option::is_none")]
131 pub interval: Option<String>,
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub timeout: Option<String>,
135 #[serde(skip_serializing_if = "Option::is_none")]
137 pub retries: Option<u32>,
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub start_period: Option<String>,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
145pub struct NormalizedVolumeDeclaration {
146 pub driver: String,
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
152pub struct NormalizedNetworkDeclaration {
153 pub driver: String,
155}
156
157fn is_false(value: &bool) -> bool {
158 !*value
159}
160
161impl NormalizedComposeConfig {
162 pub fn to_canonical_json(&self) -> Result<String, serde_json::Error> {
164 serde_json::to_string_pretty(self).map(|json| format!("{json}\n"))
165 }
166
167 pub fn service_order(&self) -> Result<Vec<String>, ComposeNormalizationError> {
169 let mut diagnostics = Vec::new();
170 for (service_name, service) in &self.services {
171 for dependency in service.depends_on.keys() {
172 if !self.services.contains_key(dependency) {
173 diagnostics.push(ComposeDiagnostic::new(
174 ComposeDiagnosticCode::InvalidValue,
175 format!(
176 "/services/{}/depends_on/{}",
177 pointer_segment(service_name),
178 pointer_segment(dependency)
179 ),
180 format!(
181 "service {service_name:?} depends on undefined service {dependency:?}"
182 ),
183 ));
184 }
185 }
186 }
187 if !diagnostics.is_empty() {
188 return Err(ComposeNormalizationError::new(diagnostics));
189 }
190
191 let mut state = BTreeMap::<String, u8>::new();
192 let mut order = Vec::new();
193 for service_name in self.services.keys() {
194 visit_service(self, service_name, &mut state, &mut order)?;
195 }
196 Ok(order)
197 }
198
199 pub fn into_config(self) -> ComposeConfig {
202 self.into()
203 }
204}
205
206fn visit_service(
207 config: &NormalizedComposeConfig,
208 service_name: &str,
209 state: &mut BTreeMap<String, u8>,
210 order: &mut Vec<String>,
211) -> Result<(), ComposeNormalizationError> {
212 match state.get(service_name) {
213 Some(1) => {
214 return Err(ComposeNormalizationError::one(ComposeDiagnostic::new(
215 ComposeDiagnosticCode::InvalidValue,
216 format!("/services/{}/depends_on", pointer_segment(service_name)),
217 format!("dependency cycle detected involving service {service_name:?}"),
218 )));
219 }
220 Some(2) => return Ok(()),
221 _ => {}
222 }
223 state.insert(service_name.to_string(), 1);
224 if let Some(service) = config.services.get(service_name) {
225 for dependency in service.depends_on.keys() {
226 visit_service(config, dependency, state, order)?;
227 }
228 }
229 state.insert(service_name.to_string(), 2);
230 order.push(service_name.to_string());
231 Ok(())
232}
233
234impl From<NormalizedComposeConfig> for ComposeConfig {
235 fn from(config: NormalizedComposeConfig) -> Self {
236 Self {
237 version: None,
238 services: config
239 .services
240 .into_iter()
241 .map(|(name, service)| (name, service.into()))
242 .collect(),
243 volumes: config
244 .volumes
245 .into_iter()
246 .map(|(name, declaration)| {
247 (
248 name,
249 Some(VolumeDeclaration {
250 driver: Some(declaration.driver),
251 }),
252 )
253 })
254 .collect(),
255 networks: config
256 .networks
257 .into_iter()
258 .map(|(name, declaration)| {
259 (
260 name,
261 Some(NetworkDeclaration {
262 driver: Some(declaration.driver),
263 }),
264 )
265 })
266 .collect(),
267 }
268 }
269}
270
271impl From<NormalizedServiceConfig> for ServiceConfig {
272 fn from(service: NormalizedServiceConfig) -> Self {
273 Self {
274 image: service.image,
275 entrypoint: service.entrypoint.map(StringOrList::List),
276 command: service.command.map(StringOrList::List),
277 environment: if service.environment.is_empty() {
278 EnvVars::Empty
279 } else {
280 EnvVars::Map(service.environment.into_iter().collect())
281 },
282 env_file: list_or_empty(service.env_file),
283 secret_environment: service.secret_environment.into_iter().collect(),
284 ports: service.ports,
285 volumes: service.volumes,
286 depends_on: if service.depends_on.is_empty() {
287 DependsOn::Empty
288 } else {
289 DependsOn::Map(
290 service
291 .depends_on
292 .into_iter()
293 .map(|(name, dependency)| {
294 (
295 name,
296 DependsOnCondition {
297 condition: dependency.condition,
298 },
299 )
300 })
301 .collect(),
302 )
303 },
304 networks: if service.networks.is_empty() {
305 ServiceNetworks::Empty
306 } else {
307 ServiceNetworks::Map(
308 service
309 .networks
310 .into_iter()
311 .map(|(name, network)| {
312 (
313 name,
314 Some(ServiceNetworkConfig {
315 aliases: network.aliases,
316 }),
317 )
318 })
319 .collect(),
320 )
321 },
322 cpus: service.cpus,
323 mem_limit: service.mem_limit,
324 restart: service.restart,
325 dns: if service.dns.is_empty() {
326 DnsConfig::Empty
327 } else {
328 DnsConfig::List(service.dns)
329 },
330 tmpfs: list_or_empty(service.tmpfs),
331 cap_add: service.cap_add,
332 cap_drop: service.cap_drop,
333 privileged: service.privileged,
334 labels: if service.labels.is_empty() {
335 Labels::Empty
336 } else {
337 Labels::Map(service.labels.into_iter().collect())
338 },
339 healthcheck: service.healthcheck.map(|healthcheck| HealthcheckConfig {
340 test: list_or_empty(healthcheck.test),
341 disable: healthcheck.disable,
342 interval: healthcheck.interval,
343 timeout: healthcheck.timeout,
344 retries: healthcheck.retries,
345 start_period: healthcheck.start_period,
346 }),
347 working_dir: service.working_dir,
348 hostname: service.hostname,
349 extra_hosts: list_or_empty(service.extra_hosts),
350 }
351 }
352}
353
354fn list_or_empty(values: Vec<String>) -> StringOrList {
355 if values.is_empty() {
356 StringOrList::Empty
357 } else {
358 StringOrList::List(values)
359 }
360}