use crate::compose::types::{
ComposeFile, ConfigConfig, SecretConfig, Service, ServiceConfigRef, ServiceSecretRef,
};
use crate::error::{ComposeError, Result};
use crate::libpod::types::container::Secret;
use super::{staging, Engine};
impl Engine {
pub(super) fn build_secret_binds(
&self,
service: &Service,
file: &ComposeFile,
) -> Result<Vec<String>> {
let mut binds = Vec::new();
for secret_ref in &service.secrets {
let (name, target_override, ref_mode, ref_uid, ref_gid) = match secret_ref {
ServiceSecretRef::Short(s) => (s.clone(), None, None, None, None),
ServiceSecretRef::Long {
source,
target,
mode,
uid,
gid,
} => (
source.clone(),
target.clone(),
*mode,
uid.clone(),
gid.clone(),
),
};
if let Some(config) = file.secrets.get(&name) {
let target = target_override.unwrap_or_else(|| format!("/run/secrets/{name}"));
match config {
SecretConfig {
file: Some(host_path),
..
} => {
let resolved =
super::container::resolve_bind_source(host_path, &self.base_dir);
binds.push(format!("{resolved}:{target}:ro"));
}
SecretConfig {
content: Some(content),
..
} => {
let path = self.materialize_inline_full(
"secrets",
&name,
content.as_bytes(),
ref_mode,
ref_uid.as_deref(),
ref_gid.as_deref(),
)?;
binds.push(format!("{}:{target}:ro", path.display()));
}
SecretConfig {
environment: Some(env_var),
..
} => {
let value = std::env::var(env_var).map_err(|_| {
ComposeError::Unsupported(format!(
"secret '{name}' references env var '{env_var}' which is not set"
))
})?;
let path = self.materialize_inline_full(
"secrets",
&name,
value.as_bytes(),
ref_mode,
ref_uid.as_deref(),
ref_gid.as_deref(),
)?;
binds.push(format!("{}:{target}:ro", path.display()));
}
_ => {}
}
}
}
Ok(binds)
}
pub(super) fn build_config_binds(
&self,
service: &Service,
file: &ComposeFile,
) -> Result<Vec<String>> {
let mut binds = Vec::new();
for config_ref in &service.configs {
let (name, target_override, ref_mode, ref_uid, ref_gid) = match config_ref {
ServiceConfigRef::Short(s) => (s.clone(), None, None, None, None),
ServiceConfigRef::Long {
source,
target,
mode,
uid,
gid,
} => (
source.clone(),
target.clone(),
*mode,
uid.clone(),
gid.clone(),
),
};
if let Some(cfg) = file.configs.get(&name) {
let target = target_override.unwrap_or_else(|| format!("/{name}"));
match cfg {
ConfigConfig {
file: Some(host_path),
..
} => {
let resolved =
super::container::resolve_bind_source(host_path, &self.base_dir);
binds.push(format!("{resolved}:{target}:ro"));
}
ConfigConfig {
content: Some(content),
..
} => {
let path = self.materialize_inline_full(
"configs",
&name,
content.as_bytes(),
ref_mode,
ref_uid.as_deref(),
ref_gid.as_deref(),
)?;
binds.push(format!("{}:{target}:ro", path.display()));
}
ConfigConfig {
environment: Some(env_var),
..
} => {
let value = std::env::var(env_var).map_err(|_| {
ComposeError::Unsupported(format!(
"config '{name}' references env var '{env_var}' which is not set"
))
})?;
let path = self.materialize_inline_full(
"configs",
&name,
value.as_bytes(),
ref_mode,
ref_uid.as_deref(),
ref_gid.as_deref(),
)?;
binds.push(format!("{}:{target}:ro", path.display()));
}
_ => {}
}
}
}
Ok(binds)
}
pub(super) async fn build_native_secrets(
&self,
service: &Service,
file: &ComposeFile,
) -> Result<Vec<Secret>> {
let secrets = collect_native_secrets(service, file)?;
for secret in &secrets {
self.ensure_external_exists("secret", "secrets", &secret.source)
.await?;
}
Ok(secrets)
}
fn materialize_inline_full(
&self,
kind: &str,
name: &str,
content: &[u8],
mode: Option<u32>,
uid: Option<&str>,
gid: Option<&str>,
) -> Result<std::path::PathBuf> {
if std::path::Path::new(name)
.components()
.any(|c| !matches!(c, std::path::Component::Normal(_)))
{
return Err(ComposeError::Unsupported(format!(
"{kind} name must not contain path separators or '..': {name}"
)));
}
let dir = self.staging_dir()?.join(kind);
staging::create_private_subdir(&dir)?;
let path = dir.join(name);
staging::write_private_file(&path, content)?;
if let Some(m) = mode {
staging::apply_mode(&path, m)?;
}
staging::apply_owner(&path, uid, gid);
Ok(path)
}
}
fn collect_native_secrets(service: &Service, file: &ComposeFile) -> Result<Vec<Secret>> {
let mut secrets = Vec::new();
for secret_ref in &service.secrets {
let (name, target_override, mode, uid, gid) = match secret_ref {
ServiceSecretRef::Short(s) => (s.clone(), None, None, None, None),
ServiceSecretRef::Long {
source,
target,
mode,
uid,
gid,
} => (
source.clone(),
target.clone(),
*mode,
uid.clone(),
gid.clone(),
),
};
if let Some(def) = file.secrets.get(&name) {
if def.external == Some(true) {
let source = def.name.clone().unwrap_or_else(|| name.clone());
let target = target_override.unwrap_or(name);
secrets.push(native_secret(source, target, mode, uid, gid)?);
}
}
}
for config_ref in &service.configs {
let (name, target_override, mode, uid, gid) = match config_ref {
ServiceConfigRef::Short(s) => (s.clone(), None, None, None, None),
ServiceConfigRef::Long {
source,
target,
mode,
uid,
gid,
} => (
source.clone(),
target.clone(),
*mode,
uid.clone(),
gid.clone(),
),
};
if let Some(def) = file.configs.get(&name) {
if def.external == Some(true) {
let source = def.name.clone().unwrap_or_else(|| name.clone());
let target = target_override.unwrap_or_else(|| format!("/{name}"));
secrets.push(native_secret(source, target, mode, uid, gid)?);
}
}
}
Ok(secrets)
}
fn native_secret(
source: String,
target: String,
mode: Option<u32>,
uid: Option<String>,
gid: Option<String>,
) -> Result<Secret> {
if let Some(m) = mode {
staging::reject_dangerous_secret_mode(m, &source)?;
}
Ok(Secret {
source,
target: Some(target),
uid: uid.and_then(|s| s.parse().ok()),
gid: gid.and_then(|s| s.parse().ok()),
mode,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::libpod::Client;
use std::path::PathBuf;
fn engine_with_base(base: &str) -> Engine {
Engine::with_base_dir(
Client::new("unused"),
"proj".to_string(),
PathBuf::from(base),
)
}
#[test]
fn secret_file_relative_path_is_anchored_to_base_dir() {
let base = PathBuf::from("/srv/project");
let yaml = "services:\n web:\n image: nginx\n secrets: [tok]\nsecrets:\n tok:\n file: secret.txt\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let engine = engine_with_base(&base.to_string_lossy());
let binds = engine
.build_secret_binds(&file.services["web"], &file)
.unwrap();
let expected = format!("{}:/run/secrets/tok:ro", base.join("secret.txt").display());
assert_eq!(binds, vec![expected]);
}
#[cfg(unix)]
#[test]
fn config_file_absolute_path_is_passed_through() {
let yaml = "services:\n web:\n image: nginx\n configs: [cfg]\nconfigs:\n cfg:\n file: /etc/app/cfg.yaml\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let engine = engine_with_base("/srv/project");
let binds = engine
.build_config_binds(&file.services["web"], &file)
.unwrap();
assert_eq!(binds, vec!["/etc/app/cfg.yaml:/cfg:ro"]);
}
#[test]
fn external_secret_becomes_native_targeting_compose_name() {
let yaml = "services:\n web:\n image: nginx\n secrets: [tok]\nsecrets:\n tok:\n external: true\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let secrets = collect_native_secrets(&file.services["web"], &file).unwrap();
assert_eq!(secrets.len(), 1);
assert_eq!(secrets[0].source, "tok");
assert_eq!(secrets[0].target.as_deref(), Some("tok"));
assert!(secrets[0].uid.is_none() && secrets[0].gid.is_none() && secrets[0].mode.is_none());
}
#[test]
fn external_secret_long_form_maps_source_target_and_perms() {
let yaml = "services:\n web:\n image: nginx\n secrets:\n - source: tok\n target: app_tok\n uid: \"100\"\n gid: \"101\"\n mode: 256\nsecrets:\n tok:\n external: true\n name: real_tok\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let secrets = collect_native_secrets(&file.services["web"], &file).unwrap();
assert_eq!(secrets.len(), 1);
let s = &secrets[0];
assert_eq!(s.source, "real_tok");
assert_eq!(s.target.as_deref(), Some("app_tok"));
assert_eq!(s.uid, Some(100));
assert_eq!(s.gid, Some(101));
assert_eq!(s.mode, Some(256));
}
#[test]
fn external_config_becomes_native_with_absolute_default_target() {
let yaml = "services:\n web:\n image: nginx\n configs: [cfg]\nconfigs:\n cfg:\n external: true\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let secrets = collect_native_secrets(&file.services["web"], &file).unwrap();
assert_eq!(secrets.len(), 1);
assert_eq!(secrets[0].source, "cfg");
assert_eq!(secrets[0].target.as_deref(), Some("/cfg"));
}
#[test]
fn non_external_secret_is_not_a_native_secret() {
let yaml = "services:\n web:\n image: nginx\n secrets: [tok]\nsecrets:\n tok:\n file: ./tok.txt\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let secrets = collect_native_secrets(&file.services["web"], &file).unwrap();
assert!(secrets.is_empty());
}
#[test]
fn non_numeric_uid_drops_to_default() {
let yaml = "services:\n web:\n image: nginx\n secrets:\n - source: tok\n uid: appuser\nsecrets:\n tok:\n external: true\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let secrets = collect_native_secrets(&file.services["web"], &file).unwrap();
assert_eq!(secrets.len(), 1);
assert!(secrets[0].uid.is_none());
}
#[test]
fn native_secret_rejects_setuid_mode() {
let yaml = "services:\n web:\n image: nginx\n secrets:\n - source: tok\n mode: 2048\nsecrets:\n tok:\n external: true\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
assert!(collect_native_secrets(&file.services["web"], &file).is_err());
}
#[test]
fn native_secret_rejects_execute_mode() {
let yaml = "services:\n web:\n image: nginx\n secrets:\n - source: tok\n mode: 511\nsecrets:\n tok:\n external: true\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
assert!(collect_native_secrets(&file.services["web"], &file).is_err());
}
#[test]
fn native_config_rejects_setgid_mode() {
let yaml = "services:\n web:\n image: nginx\n configs:\n - source: cfg\n mode: 1024\nconfigs:\n cfg:\n external: true\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
assert!(collect_native_secrets(&file.services["web"], &file).is_err());
}
#[test]
fn native_secret_allows_world_readable_mode() {
let yaml = "services:\n web:\n image: nginx\n secrets:\n - source: tok\n mode: 292\nsecrets:\n tok:\n external: true\n";
let file = crate::compose::parse_str_raw(yaml).unwrap();
let secrets = collect_native_secrets(&file.services["web"], &file).unwrap();
assert_eq!(secrets[0].mode, Some(0o444));
}
}