compose_validatr/
compose.rs

1//! Compose fields and validation
2
3use std::{collections::HashMap, fmt::Display};
4
5use crate::{
6    configs::Config,
7    errors::{ValidationError, ValidationErrors},
8    networks::Network,
9    secrets::Secret,
10    services::Service,
11    volumes::Volume,
12};
13
14use super::{configs, networks, secrets, services, volumes};
15use serde::{Deserialize, Serialize};
16use serde_yaml;
17
18/// Represents an entire [Docker Compose](https://docs.docker.com/compose/compose-file/) manifest
19///
20/// All fields other than the `services` field are optional. Optional fields are skipped
21/// from serialization if they are `None`
22#[derive(Debug, Clone, Deserialize, Serialize)]
23pub struct Compose {
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub version: Option<String>,
26
27    pub services: HashMap<String, services::Service>,
28
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub networks: Option<HashMap<String, Option<networks::Network>>>,
31
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub volumes: Option<HashMap<String, Option<volumes::Volume>>>,
34
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub configs: Option<HashMap<String, Option<configs::Config>>>,
37
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub secrets: Option<HashMap<String, Option<secrets::Secret>>>,
40}
41
42impl Compose {
43    /// Create and validate a [`Compose`] representation
44    pub fn new(contents: &str) -> Result<Self, ValidationErrors> {
45        let mut errors = ValidationErrors::new();
46        let compose: Result<Self, ValidationError> = serde_yaml::from_str(contents)
47            .map_err(|e| ValidationError::InvalidCompose(e.to_string()));
48
49        match compose {
50            Ok(c) => {
51                if let Some(networks) = &c.networks {
52                    Self::validate_networks(&c, networks, &mut errors);
53                };
54                if let Some(volumes) = &c.volumes {
55                    Self::validate_volumes(&c, volumes, &mut errors);
56                };
57                if let Some(configs) = &c.configs {
58                    Self::validate_configs(&c, configs, &mut errors);
59                };
60                if let Some(secrets) = &c.secrets {
61                    Self::validate_secrets(&c, secrets, &mut errors);
62                };
63                Self::validate_services(&c, &c.services, &mut errors);
64                if errors.has_errors() {
65                    return Err(errors);
66                }
67                Ok(c)
68            }
69            Err(err) => {
70                errors.add_error(err);
71                Err(errors)
72            }
73        }
74    }
75
76    /// Validate top level networks
77    fn validate_networks(
78        compose: &Compose,
79        networks: &HashMap<String, Option<Network>>,
80        errors: &mut ValidationErrors,
81    ) {
82        for (_, network_attributes) in networks {
83            if let Some(network) = network_attributes {
84                network.validate(compose, errors);
85            }
86        }
87    }
88
89    /// Validate top level volumes
90    fn validate_volumes(
91        compose: &Compose,
92        volumes: &HashMap<String, Option<Volume>>,
93        errors: &mut ValidationErrors,
94    ) {
95        for (_, volume_attributes) in volumes {
96            if let Some(volume) = volume_attributes {
97                volume.validate(compose, errors);
98            }
99        }
100    }
101
102    /// Validate top level configs
103    fn validate_configs(
104        compose: &Compose,
105        configs: &HashMap<String, Option<Config>>,
106        errors: &mut ValidationErrors,
107    ) {
108        for (_, config_attributes) in configs {
109            if let Some(config) = config_attributes {
110                config.validate(compose, errors);
111            }
112        }
113    }
114
115    /// Validate top level secrets
116    fn validate_secrets(
117        compose: &Compose,
118        secrets: &HashMap<String, Option<Secret>>,
119        errors: &mut ValidationErrors,
120    ) {
121        for (_, secret_attributes) in secrets {
122            if let Some(secret) = secret_attributes {
123                secret.validate(compose, errors);
124            }
125        }
126    }
127
128    /// Validate services
129    fn validate_services(
130        compose: &Compose,
131        services: &HashMap<String, Service>,
132        errors: &mut ValidationErrors,
133    ) {
134        for (_, service) in services {
135            service.validate(compose, errors);
136        }
137    }
138}
139
140/// This trait needs to be implemented for top level elements
141pub(crate) trait Validate {
142    /// Validate that an attribute is valid within the context of the compose manifest
143    ///
144    /// Push all validation errors to the ValidationErrors so that users are able to see
145    /// all of their errors at once, versus incrementally
146    fn validate(&self, ctx: &Compose, errors: &mut ValidationErrors);
147}
148
149impl Display for Compose {
150    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151        write!(f, "{}", serde_yaml::to_string(&self).unwrap())
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn simple_compose() {
161        let yaml = r#"
162        "#;
163
164        let compose = Compose::new(yaml);
165        assert!(compose.is_err());
166    }
167
168    #[test]
169    fn big_compose() {
170        let yaml = r#"
171        version: '3.9'
172
173        services:
174          gitlab:
175            image: gitlab/gitlab-ce:latest
176            container_name: gitlab
177            hostname: gitlab
178            restart: always
179            depends_on:
180              - postgres
181            ports:
182              - "8080:80"
183              - "8443:443"
184              - "8022:22"
185            environment:
186              GITLAB_ROOT_PASSWORD: eYPkjBbrtzX8eGVc
187              DATABASE_URL: "postgres://gitlab:eYPkjBbrtzX8eGVc@postgres:5432/gitlab"
188            volumes:
189              - ./gitlab/config:/etc/gitlab
190              - ./gitlab/logs:/var/log/gitlab
191              - ./gitlab/data:/var/opt/gitlab
192            shm_size: '256m'
193        
194          registry:
195            image: registry:2
196            container_name: registry
197            hostname: registry
198            ports:
199              - "5000:5000"
200            volumes:
201              - registry:/var/lib/registry
202        
203          sonarqube:
204            build:
205              context: ./sonarqube_image
206            container_name: sonarqube
207            hostname: sonarqube
208            restart: always
209            ports:
210              - "9000:9000"
211              - "9092:9092"
212            volumes:
213              - sonarqube:/opt/sonarqube/data
214              - sonarqube:/opt/sonarqube/logs
215              - sonarqube:/opt/sonarqube/extensions
216        
217          jenkins:
218            build:
219              context: ./jenkins_image
220            container_name: jenkins
221            hostname: jenkins
222            restart: always
223            ports:
224              - "9080:8080"
225              - "50000:50000"
226            volumes:
227              - jenkins:/var/jenkins_home
228              - jenkins-data:/var/jenkins_home
229              - jenkins-docker-certs:/certs/client:ro
230            environment:
231              - JAVA_OPTS=-Djenkins.install.runSetupWizard=false
232              - DOCKER_HOST=tcp://docker:2376
233              - DOCKER_CERT_PATH=/certs/client
234              - DOCKER_TLS_VERIFY=1
235        
236          jenkins-docker:
237            image: docker:dind
238            container_name: jenkins-docker
239            hostname: docker
240            privileged: true
241            environment:
242              - DOCKER_TLS_CERTDIR=/certs
243            volumes:
244              - /etc/docker/daemon.json:/etc/docker/daemon.json
245              - jenkins-docker-certs:/certs/client
246              - jenkins-data:/var/jenkins_home
247            ports:
248              - '2376:2376'
249            command: --storage-driver overlay2
250        
251          postgres:
252            image: postgres:latest
253            container_name: postgres
254            hostname: postgres
255            restart: always
256            ports:
257              - "5432:5432"
258            volumes:
259              - postgres:/var/lib/postgresql/data
260            environment:
261              POSTGRES_DB: gitlab
262              POSTGRES_USER: gitlab
263              POSTGRES_PASSWORD: eYPkjBbrtzX8eGVc
264        
265        volumes:
266          sonarqube:
267          jenkins:
268          jenkins-docker-certs:
269          jenkins-data:
270          postgres:
271          registry:
272        
273        networks:
274          default:
275            driver: bridge
276        "#;
277        let compose = Compose::new(yaml);
278        assert!(compose.is_ok());
279    }
280}