1mod blkio_config;
4mod build;
5mod deploy;
6mod healthcheck;
7mod logging;
8mod networks;
9mod ports;
10mod secrets;
11mod volumes;
12
13use crate::{compose::Compose, errors::ValidationError};
14use regex::Regex;
15use std::collections::HashMap;
16
17use serde::{Deserialize, Serialize};
18
19use crate::{compose::Validate, errors::ValidationErrors};
20
21#[derive(Debug, Clone, Deserialize, Serialize)]
23pub struct Service {
24 #[serde(skip_serializing_if = "Option::is_none")]
25 pub attach: Option<bool>,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub build: Option<build::Build>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
31 pub blkio_config: Option<blkio_config::BlkioConfig>,
32
33 #[serde(skip_serializing_if = "Option::is_none")]
34 pub cpu_count: Option<u8>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub cpu_percent: Option<f32>,
38
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub cpu_shares: Option<u32>,
41
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub cpu_period: Option<String>,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub cpu_quota: Option<String>,
47
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub cpu_rt_runtime: Option<String>,
50
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub cpu_rt_period: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub cpus: Option<f32>, #[serde(skip_serializing_if = "Option::is_none")]
58 pub cpuset: Option<u8>,
59
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub cap_add: Option<Vec<Capabilities>>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
64 pub cap_drop: Option<Vec<Capabilities>>,
65
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub cgroup: Option<Cgroup>,
68
69 #[serde(skip_serializing_if = "Option::is_none")]
70 pub cgroup_parent: Option<String>,
71
72 #[serde(skip_serializing_if = "Option::is_none")]
73 pub command: Option<Command>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub configs: Option<Vec<Config>>,
77
78 #[serde(skip_serializing_if = "Option::is_none")]
79 pub container_name: Option<String>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub credential_spec: Option<CredentialSpec>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
85 pub depends_on: Option<DependsOn>,
86
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub deploy: Option<deploy::Deploy>,
89
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub device_cgroup_rules: Option<Vec<String>>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub devices: Option<Vec<String>>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub dns: Option<Labels>, #[serde(skip_serializing_if = "Option::is_none")]
100 pub dns_opt: Option<Vec<String>>,
101
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub dns_search: Option<Labels>,
104
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub domainname: Option<String>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub entrypoint: Option<Labels>,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
112 pub env_file: Option<Labels>,
113
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub environment: Option<Labels>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
118 pub expose: Option<Vec<String>>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub extends: Option<Extends>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
124 pub annotations: Option<Labels>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
127 pub external_links: Option<Vec<String>>,
128
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub extra_hosts: Option<Labels>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
133 pub group_add: Option<Vec<String>>,
134
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub healthcheck: Option<healthcheck::HealthCheck>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub hostname: Option<String>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
142 pub image: Option<String>,
143
144 #[serde(skip_serializing_if = "Option::is_none")]
145 pub init: Option<bool>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub ipc: Option<String>,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
151 pub uts: Option<String>,
152
153 #[serde(skip_serializing_if = "Option::is_none")]
154 pub isolation: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
157 pub labels: Option<Labels>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
160 pub links: Option<Vec<String>>,
161
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub logging: Option<logging::Logging>,
164
165 #[serde(skip_serializing_if = "Option::is_none")]
166 pub network_mode: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
169 pub networks: Option<networks::Networks>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub mac_address: Option<String>,
173
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub mem_limit: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
178 pub mem_reservation: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
181 pub mem_swappiness: Option<u8>,
182
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub memswap_limit: Option<String>,
185
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub oom_kill_disable: Option<bool>,
188
189 #[serde(skip_serializing_if = "Option::is_none")]
190 pub oom_score_adj: Option<i16>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
193 pub pid: Option<u32>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub pids_limit: Option<u32>,
197
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub platform: Option<String>,
200
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub ports: Option<ports::Ports>,
203
204 #[serde(skip_serializing_if = "Option::is_none")]
205 pub privileged: Option<bool>,
206
207 #[serde(skip_serializing_if = "Option::is_none")]
208 pub profiles: Option<Vec<String>>,
209
210 #[serde(skip_serializing_if = "Option::is_none")]
211 pub pull_policy: Option<PullPolicy>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
214 pub read_only: Option<bool>,
215
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub restart: Option<Restart>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
220 pub runtime: Option<String>,
221
222 #[serde(skip_serializing_if = "Option::is_none")]
223 pub scale: Option<u32>, #[serde(skip_serializing_if = "Option::is_none")]
226 pub secrets: Option<Vec<secrets::Secret>>,
227
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub secruity_opt: Option<Vec<String>>,
230
231 #[serde(skip_serializing_if = "Option::is_none")]
232 pub shm_size: Option<String>,
233
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub stdin_open: Option<String>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
238 pub stop_grace_period: Option<String>,
239
240 #[serde(skip_serializing_if = "Option::is_none")]
241 pub stop_signal: Option<String>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
244 pub storage_opt: Option<String>,
245
246 #[serde(skip_serializing_if = "Option::is_none")]
247 pub sysctls: Option<Labels>,
248
249 #[serde(skip_serializing_if = "Option::is_none")]
250 pub tmpfs: Option<Tmpfs>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
253 pub tty: Option<String>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
256 pub ulimits: Option<Ulimits>,
257
258 #[serde(skip_serializing_if = "Option::is_none")]
259 pub user: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub userns_mode: Option<String>,
263
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub volumes: Option<Vec<volumes::Volumes>>,
266
267 #[serde(skip_serializing_if = "Option::is_none")]
268 pub volumes_from: Option<Vec<String>>,
269
270 #[serde(skip_serializing_if = "Option::is_none")]
271 pub working_dir: Option<String>,
272}
273
274#[derive(Debug, Clone, Deserialize, Serialize)]
275#[serde(untagged)]
276pub enum Labels {
277 List(Vec<String>),
278 Map(HashMap<String, String>),
279}
280
281#[derive(Debug, Clone, Deserialize, Serialize)]
282#[serde(untagged)]
283pub enum Tmpfs {
284 String(String),
285 List(Vec<String>),
286}
287
288#[derive(Debug, Clone, Deserialize, Serialize)]
289#[serde(untagged)]
290pub enum Config {
291 Short(String),
292 Long(ConfigDetails),
293}
294
295#[derive(Debug, Clone, Deserialize, Serialize)]
296pub struct ConfigDetails {
297 pub source: String,
298 pub target: String,
299 pub uid: String,
300 pub gid: String,
301 pub mode: String,
302}
303
304#[derive(Debug, Clone, Deserialize, Serialize)]
305#[serde(rename_all = "lowercase")]
306pub enum Cgroup {
307 Host,
308 Private,
309}
310
311#[derive(Debug, Clone, Deserialize, Serialize)]
312#[serde(untagged)]
313pub enum Command {
314 String(String),
315 List(Vec<String>),
316}
317
318#[derive(Debug, Clone, Deserialize, Serialize)]
319pub struct CredentialSpec {
320 #[serde(skip_serializing_if = "Option::is_none")]
321 pub file: Option<String>,
322
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub registry: Option<String>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
327 pub config: Option<String>, }
329
330#[derive(Debug, Clone, Deserialize, Serialize)]
331#[serde(untagged)]
332pub enum DependsOn {
333 List(Vec<String>), Map(HashMap<String, DependsOnDetail>),
335}
336
337#[derive(Debug, Clone, Deserialize, Serialize)]
338pub struct DependsOnDetail {
339 #[serde(skip_serializing_if = "Option::is_none")]
340 pub restart: Option<bool>,
341
342 #[serde(skip_serializing_if = "Option::is_none")]
343 pub condition: Option<DependsOnCondition>,
344
345 #[serde(skip_serializing_if = "Option::is_none")]
346 pub required: Option<bool>,
347}
348
349#[derive(Debug, Clone, Deserialize, Serialize)]
350#[serde(rename_all = "snake_case")]
351pub enum DependsOnCondition {
352 ServiceStarted,
353 ServiceHealthy,
354 ServiceCompletedSuccessfully,
355}
356
357#[derive(Debug, Clone, Deserialize, Serialize)]
358pub struct Extends {
359 pub file: String,
361 pub service: String,
362}
363
364#[derive(Debug, Clone, Deserialize, Serialize)]
365#[serde(rename_all = "lowercase")]
366pub enum PullPolicy {
367 Always,
368 Never,
369 Missing,
370 Build,
371}
372
373#[derive(Debug, Clone, Deserialize, Serialize)]
374#[serde(rename_all = "lowercase")]
375pub enum Restart {
376 No,
377 Always,
378 OnFailure,
379 UnlessStopped,
380}
381
382#[derive(Debug, Clone, Deserialize, Serialize)]
383pub struct Ulimits {
384 pub nproc: u16,
385 pub nofile: Nofile,
386}
387
388#[derive(Debug, Clone, Deserialize, Serialize)]
389pub struct Nofile {
390 pub soft: u16,
391 pub hard: u16,
392}
393
394#[derive(Debug, Clone, Deserialize, Serialize)]
395#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
396pub enum Capabilities {
397 All,
398 AuditControl,
399 AuditRead,
400 AuditWrite,
401 BlockSuspend,
402 Bpf,
403 CheckpointRestore,
404 Chown,
405 DacOverride,
406 DacReadSearch,
407 Fowner,
408 Fsetid,
409 IpcLock,
410 IpcOwner,
411 Kill,
412 Lease,
413 LinuxImmutable,
414 MacAdmin,
415 MacOverride,
416 Mknod,
417 NetAdmin,
418 NetBindService,
419 NetBroadcast,
420 NetRaw,
421 Perfmon,
422 Setgid,
423 Setfcap,
424 Setpcap,
425 Setuid,
426 SysAdmin,
427 SysBoot,
428 SysChroot,
429 SysModule,
430 SysNice,
431 SysPacct,
432 SysPtrace,
433 SysRawio,
434 SysResource,
435 SysTime,
436 SysTtyConfig,
437 Syslog,
438 WakeAlarm,
439}
440
441impl Service {
442 fn validate_blkio_config(&self, ctx: &Compose, errors: &mut ValidationErrors) {
443 self.blkio_config.as_ref().map(|b| b.validate(ctx, errors));
444 }
445
446 fn validate_build(&self, ctx: &Compose, errors: &mut ValidationErrors) {
447 self.build.as_ref().map(|b| b.validate(ctx, errors));
448 }
449
450 fn validate_deploy(&self, ctx: &Compose, errors: &mut ValidationErrors) {
451 self.deploy.as_ref().map(|d| d.validate(ctx, errors));
452 }
453
454 fn validate_healthcheck(&self, ctx: &Compose, errors: &mut ValidationErrors) {
455 self.healthcheck.as_ref().map(|h| h.validate(ctx, errors));
456 }
457
458 fn validate_logging(&self, ctx: &Compose, errors: &mut ValidationErrors) {
459 self.logging.as_ref().map(|l| l.validate(ctx, errors));
460 }
461
462 fn validate_networks(&self, ctx: &Compose, errors: &mut ValidationErrors) {
463 self.networks.as_ref().map(|n| n.validate(ctx, errors));
464 }
465
466 fn validate_ports(&self, ctx: &Compose, errors: &mut ValidationErrors) {
467 self.ports.as_ref().map(|p| p.validate(ctx, errors));
468 }
469
470 fn validate_secrets(&self, ctx: &Compose, errors: &mut ValidationErrors) {
471 self.secrets
472 .as_ref()
473 .map(|s| s.iter().for_each(|s| s.validate(ctx, errors)));
474 }
475
476 fn validate_volumes(&self, ctx: &Compose, errors: &mut ValidationErrors) {
477 self.volumes
478 .as_ref()
479 .map(|v| v.iter().for_each(|volume| volume.validate(ctx, errors)));
480 }
481
482 fn validate_configs(&self, ctx: &Compose, errors: &mut ValidationErrors) {
483 self.configs.as_ref().map(|c| {
486 c.iter().for_each(|config| match config {
487 Config::Short(c) => {
488 ctx.configs.as_ref().map(|configs| {
489 if !configs.contains_key(c) {
490 errors.add_error(ValidationError::InvalidValue(format!(
491 "Config is not defined: {}",
492 c
493 )))
494 }
495 });
496 }
497 Config::Long(c) => {
498 ctx.configs.as_ref().map(|configs| {
499 if !configs.contains_key(&c.source) {
500 errors.add_error(ValidationError::InvalidValue(format!(
501 "Config is not defined: {}",
502 &c.source
503 )))
504 }
505 });
506 }
507 })
508 });
509 }
510
511 fn validate_credential_spec(&self, ctx: &Compose, errors: &mut ValidationErrors) {
512 self.credential_spec.as_ref().map(|c| {
513 let result = c.config.as_ref().map(|cred| {
514 ctx.configs
515 .as_ref()
516 .map(|configs| configs.contains_key(cred))
517 .is_some()
518 });
519 result.map(|r| {
520 if !r {
521 errors.add_error(ValidationError::InvalidValue(
522 "Credential Spec references unknown config".to_string(),
523 ))
524 }
525 });
526 });
527 }
528
529 fn validate_container_name(&self, _: &Compose, errors: &mut ValidationErrors) {
530 let re = Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9_.-]+$").unwrap();
531 self.container_name.as_ref().map(|c| {
532 if !re.is_match(c) {
533 errors.add_error(ValidationError::InvalidValue(
534 "Invalid container name".to_string(),
535 ));
536 }
537 });
538 }
539
540 fn validate_depends_on(&self, ctx: &Compose, errors: &mut ValidationErrors) {
541 self.depends_on.as_ref().map(|d| match d {
542 DependsOn::List(service) => {
543 let result = service.iter().any(|s| ctx.services.contains_key(s));
544 if !result {
545 errors.add_error(ValidationError::InvalidValue(
546 "Invalid service for depends_on".to_string(),
547 ));
548 }
549 }
550 DependsOn::Map(service) => {
551 let result = service.iter().any(|s| ctx.services.contains_key(s.0));
552 if !result {
553 errors.add_error(ValidationError::InvalidValue(
554 "Invalid service for depends_on".to_string(),
555 ));
556 }
557 }
558 });
559 }
560
561 fn validate_expose(&self, _: &Compose, errors: &mut ValidationErrors) {
562 self.expose.as_ref().map(|e| {
563 e.iter().all(|port| match port.parse::<u16>() {
564 Err(_) => {
565 errors.add_error(ValidationError::InvalidValue("Invalid port".to_string()));
566 false
567 }
568 _ => true,
569 })
570 });
571 }
572
573 fn validate_extends(&self, ctx: &Compose, errors: &mut ValidationErrors) {
574 self.extends.as_ref().map(|e| {
575 let result = ctx.services.contains_key(&e.service);
576 if result {
577 let service = ctx.services.get(&e.service).unwrap();
582 service.depends_on.as_ref().map(|_| {
583 errors.add_error(ValidationError::InvalidValue(
584 "Extends cannot extend another service that has a depends_on".to_string(),
585 ))
586 });
587 service.links.as_ref().map(|l| {
588 if l.len() > 0 {
589 errors.add_error(ValidationError::InvalidValue(
590 "Extends cannot have any links".to_string(),
591 ))
592 }
593 });
594 service.volumes_from.as_ref().map(|v| {
595 if v.len() > 0 {
596 errors.add_error(ValidationError::InvalidValue(
597 "Extends cannot have any volumes_from".to_string(),
598 ))
599 }
600 });
601 service.ipc.as_ref().map(|_| {
602 errors.add_error(ValidationError::InvalidValue(
603 "Extends cannot have an IPC mode".to_string(),
604 ))
605 });
606 service.network_mode.as_ref().map(|n| {
607 if n.starts_with("service:") {
608 errors.add_error(ValidationError::InvalidValue(
609 "Extends cannot extend a service that has a network dependency"
610 .to_string(),
611 ))
612 }
613 });
614 } else {
615 errors.add_error(ValidationError::InvalidValue(
616 "Extends references invalid service".to_string(),
617 ));
618 }
619 });
620 }
621}
622
623impl Validate for Service {
624 fn validate(&self, ctx: &Compose, errors: &mut ValidationErrors) {
625 self.validate_blkio_config(ctx, errors);
626 self.validate_build(ctx, errors);
627 self.validate_deploy(ctx, errors);
628 self.validate_healthcheck(ctx, errors);
629 self.validate_logging(ctx, errors);
630 self.validate_networks(ctx, errors);
631 self.validate_ports(ctx, errors);
632 self.validate_secrets(ctx, errors);
633 self.validate_volumes(ctx, errors);
634 self.validate_configs(ctx, errors);
635 self.validate_container_name(ctx, errors);
636 self.validate_credential_spec(ctx, errors);
637 self.validate_depends_on(ctx, errors);
638 self.validate_expose(ctx, errors);
639 self.validate_extends(ctx, errors);
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 use super::*;
646
647 #[test]
648 fn depends_on_missing_service() {
649 let yaml = r#"
650 services:
651 gitlab:
652 image: gitlab/gitlab-ce:latest
653 container_name: gitlab
654 hostname: gitlab
655 restart: always
656 depends_on:
657 - hello_world
658 "#;
659
660 let compose = Compose::new(yaml);
661 assert!(compose.is_err());
662 assert!(compose.is_err_and(|e| e.all_errors().len() == 1));
663 }
664
665 #[test]
666 fn depends_on_valid_service() {
667 let yaml = r#"
668 services:
669 gitlab:
670 image: gitlab/gitlab-ce:latest
671 container_name: gitlab
672 hostname: gitlab
673 restart: always
674 depends_on:
675 - hello_world
676 hello_world:
677 image: gitlab/gitlab-ce:latest
678 container_name: gitlab
679 hostname: gitlab
680 restart: always
681 "#;
682
683 let compose = Compose::new(yaml);
684 assert!(compose.is_ok());
685 }
686
687 #[test]
688 fn valid_config() {
689 let yaml = r#"
690 services:
691 gitlab:
692 image: gitlab/gitlab-ce:latest
693 container_name: gitlab
694 hostname: gitlab
695 restart: always
696 configs:
697 - my_config
698 - my_other_config
699 configs:
700 my_config:
701 file: ./my_config.txt
702 my_other_config:
703 external: true
704 "#;
705
706 let compose = Compose::new(yaml);
707 assert!(compose.is_ok());
708 }
709
710 #[test]
711 fn invalid_config() {
712 let yaml = r#"
713 services:
714 gitlab:
715 image: gitlab/gitlab-ce:latest
716 container_name: gitlab
717 hostname: gitlab
718 restart: always
719 configs:
720 - hello
721 - world
722 configs:
723 my_config:
724 file: ./my_config.txt
725 my_other_config:
726 external: true
727 "#;
728
729 let compose = Compose::new(yaml);
730 dbg!(&compose);
731 assert!(compose.is_err());
732 }
733}