use crate::compose::types::ComposeFile;
use crate::error::{ComposeError, Result};
use crate::libpod::is_valid_object_name;
use super::network::resolve_network_name;
use super::Engine;
impl Engine {
pub(super) fn validate_object_names(&self, file: &ComposeFile) -> Result<()> {
validate_object_names(file, &self.project)
}
}
fn validate_object_names(file: &ComposeFile, project: &str) -> Result<()> {
for (key, config) in &file.volumes {
if config.as_ref().and_then(|c| c.external).unwrap_or(false) {
continue;
}
let name = config
.as_ref()
.and_then(|c| c.name.as_deref())
.map(str::to_string)
.unwrap_or_else(|| format!("{project}_{key}"));
ensure_valid_object_name("volume", key, &name)?;
}
for (key, config) in &file.networks {
if config.as_ref().and_then(|c| c.external).unwrap_or(false) {
continue;
}
let name = resolve_network_name(key, file, project);
ensure_valid_object_name("network", key, &name)?;
}
for (svc_name, service) in &file.services {
let name = service
.container_name
.clone()
.unwrap_or_else(|| format!("{project}-{svc_name}"));
ensure_valid_object_name("container", svc_name, &name)?;
}
Ok(())
}
pub(super) fn ensure_valid_object_name(kind: &str, key: &str, name: &str) -> Result<()> {
if is_valid_object_name(name) {
return Ok(());
}
let origin = if name == key {
String::new()
} else {
format!(" (from {kind} '{key}')")
};
Err(ComposeError::Unsupported(format!(
"invalid {kind} name {name:?}{origin}: names must start with an ASCII letter or digit \
and contain only letters, digits, '_', '.', '-'"
)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parse_str_raw;
fn file(yaml: &str) -> ComposeFile {
parse_str_raw(yaml).unwrap()
}
#[test]
fn clean_project_passes() {
let f =
file("services:\n web:\n image: nginx\nvolumes:\n data:\nnetworks:\n back:\n");
validate_object_names(&f, "proj").unwrap();
}
#[test]
fn bad_explicit_volume_name_is_rejected() {
let f = file(
"services:\n web:\n image: nginx\nvolumes:\n badvol:\n name: \"x@bad name!\"\n",
);
let err = validate_object_names(&f, "proj").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("invalid volume name"), "got: {msg}");
assert!(msg.contains("x@bad name!"), "got: {msg}");
assert!(msg.contains("badvol"), "got: {msg}");
}
#[test]
fn bad_default_volume_name_via_key_is_rejected() {
let f = file("services:\n web:\n image: nginx\nvolumes:\n bad key:\n");
let err = validate_object_names(&f, "proj").unwrap_err();
assert!(
err.to_string().contains("invalid volume name"),
"got: {err}"
);
}
#[test]
fn bad_explicit_network_name_is_rejected() {
let f =
file("services:\n web:\n image: nginx\nnetworks:\n net:\n name: \"bad@net\"\n");
let err = validate_object_names(&f, "proj").unwrap_err();
let msg = err.to_string();
assert!(msg.contains("invalid network name"), "got: {msg}");
assert!(msg.contains("bad@net"), "got: {msg}");
}
#[test]
fn bad_container_name_is_rejected() {
let f = file("services:\n web:\n image: nginx\n container_name: \"bad name!\"\n");
let err = validate_object_names(&f, "proj").unwrap_err();
assert!(
err.to_string().contains("invalid container name"),
"got: {err}"
);
}
#[test]
fn external_resources_are_not_name_validated() {
let f = file(
"services:\n web:\n image: nginx\nvolumes:\n ext:\n external: true\n name: \"weird:name\"\nnetworks:\n en:\n external: true\n name: \"weird:net\"\n",
);
validate_object_names(&f, "proj").unwrap();
}
#[test]
fn origin_is_omitted_when_name_equals_key() {
let err = ensure_valid_object_name("volume", "bad name", "bad name").unwrap_err();
assert!(!err.to_string().contains("(from"), "got: {err}");
}
}