use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::{
compose::{Compose, Validate},
errors::ValidationError,
};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Build {
String(String),
Map(BuildDetails),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct BuildDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dockerfile: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dockerfile_inline: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<BuildArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ssh: Option<SshArgs>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_from: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_to: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_contexts: Option<AdditionalContexts>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_hosts: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub isolation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub privileged: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub labels: Option<Labels>,
#[serde(skip_serializing_if = "Option::is_none")]
pub no_cache: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pull: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shm_size: Option<ShmSize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub secrets: Option<Vec<BuildSecret>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub platforms: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum BuildArgs {
List(Vec<String>),
Map(HashMap<String, String>),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum SshArgs {
List(Vec<String>),
Map(HashMap<String, String>),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum AdditionalContexts {
List(Vec<String>),
Map(HashMap<String, String>),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum Labels {
List(Vec<String>),
Map(HashMap<String, String>),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum ShmSize {
String(String),
Bytes(u64),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum BuildSecret {
Short(String),
Long(SecretDetails),
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SecretDetails {
pub source: String,
pub target: String,
pub uid: String,
pub gid: String,
pub mode: String,
}
impl Validate for Build {
fn validate(&self, ctx: &Compose, errors: &mut crate::errors::ValidationErrors) {
match self {
Build::Map(details) => {
details.secrets.as_ref().map(|s| {
s.iter().for_each(|secret| match secret {
BuildSecret::Short(short) => {
ctx.secrets.as_ref().map(|x| {
if !x.contains_key(short) {
errors.add_error(ValidationError::InvalidValue(format!(
"Secret is not defined: {}",
short
)))
}
});
}
BuildSecret::Long(details) => {
ctx.secrets.as_ref().map(|x| {
if !x.contains_key(&details.source) {
errors.add_error(ValidationError::InvalidValue(format!(
"Secret is not defined: {}",
details.source
)))
}
});
}
})
});
}
_ => (),
}
match self {
Build::Map(details) => {
if details.dockerfile_inline.is_some() && details.dockerfile.is_some() {
errors.add_error(ValidationError::InvalidValue(
"Cannot specify a Dockerfile and an inline Dockerfile".to_string(),
))
}
}
_ => (),
}
()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_build() {
let yaml = r#"
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
hostname: gitlab
restart: always
build:
context: .
dockerfile: webapp.Dockerfile
"#;
let compose = Compose::new(yaml);
assert!(compose.is_ok())
}
#[test]
fn test_invalid_build_inline() {
let yaml = r#"
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
hostname: gitlab
restart: always
build:
context: .
dockerfile: webapp.Dockerfile
dockerfile_inline: |
FROM baseimage
RUN some command
"#;
let compose = Compose::new(yaml);
assert!(compose.is_err());
assert!(compose.is_err_and(|e| e.all_errors().len() == 1));
}
#[test]
fn test_valid_secret_reference() {
let yaml = r#"
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
hostname: gitlab
restart: always
build:
context: .
dockerfile: webapp.Dockerfile
secrets:
- server-certificate
secrets:
server-certificate:
file: ./server.cert
"#;
let compose = Compose::new(yaml);
assert!(compose.is_ok());
}
#[test]
fn test_invalid_secret_reference() {
let yaml = r#"
services:
gitlab:
image: gitlab/gitlab-ce:latest
container_name: gitlab
hostname: gitlab
restart: always
build:
context: .
dockerfile: webapp.Dockerfile
secrets:
- server-certificate
secrets:
hello-world:
file: ./server.cert
"#;
let compose = Compose::new(yaml);
assert!(compose.is_err());
}
}