use std::collections::HashSet;
use crate::compose::types::{ComposeFile, Service};
pub fn retain_active_profiles(file: &mut ComposeFile, active: &[String]) {
retain_active_profiles_with_targets(file, active, &[]);
}
pub fn retain_active_profiles_with_targets(
file: &mut ComposeFile,
active: &[String],
targets: &[String],
) {
let set = active_profiles_set(active);
let named: HashSet<&str> = targets.iter().map(|s| s.as_str()).collect();
let mut enabled: HashSet<String> = file
.services
.iter()
.filter(|(name, svc)| service_in_profiles(svc, &set) || named.contains(name.as_str()))
.map(|(name, _)| name.clone())
.collect();
let mut stack: Vec<String> = enabled.iter().cloned().collect();
while let Some(name) = stack.pop() {
if let Some(svc) = file.services.get(&name) {
for dep in svc.depends_on.service_names() {
if file.services.contains_key(&dep) && enabled.insert(dep.clone()) {
stack.push(dep);
}
}
}
}
file.services.retain(|name, _| enabled.contains(name));
}
pub(super) fn active_profiles_set(active: &[String]) -> HashSet<String> {
if !active.is_empty() {
return active.iter().cloned().collect();
}
std::env::var("COMPOSE_PROFILES")
.ok()
.map(|s| {
s.split(',')
.map(|p| p.trim().to_string())
.filter(|p| !p.is_empty())
.collect()
})
.unwrap_or_default()
}
pub(super) fn service_in_profiles(service: &Service, active: &HashSet<String>) -> bool {
if service.profiles.is_empty() {
return true;
}
if active.contains("*") {
return true;
}
service.profiles.iter().any(|p| active.contains(p))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compose::types::Service;
#[test]
fn explicit_profiles_ignores_env() {
let set = active_profiles_set(&["prod".to_string()]);
assert!(set.contains("prod"));
assert_eq!(set.len(), 1);
}
#[test]
fn empty_slice_with_no_env_returns_empty() {
temp_env::with_var_unset("COMPOSE_PROFILES", || {
let set = active_profiles_set(&[]);
assert!(set.is_empty());
});
}
#[test]
fn empty_slice_falls_back_to_env_var() {
temp_env::with_var("COMPOSE_PROFILES", Some(" debug , , prod "), || {
let set = active_profiles_set(&[]);
assert_eq!(set.len(), 2);
assert!(set.contains("debug"));
assert!(set.contains("prod"));
});
}
#[test]
fn service_with_no_profiles_always_runs() {
let svc = Service::default();
let active: HashSet<String> = HashSet::new();
assert!(service_in_profiles(&svc, &active));
}
#[test]
fn service_profile_matches_active() {
let svc = Service {
profiles: vec!["debug".to_string()],
..Default::default()
};
let active: HashSet<String> = ["debug".to_string()].into();
assert!(service_in_profiles(&svc, &active));
}
#[test]
fn service_profile_does_not_match() {
let svc = Service {
profiles: vec!["debug".to_string()],
..Default::default()
};
let active: HashSet<String> = ["prod".to_string()].into();
assert!(!service_in_profiles(&svc, &active));
}
#[test]
fn service_any_profile_match_sufficient() {
let svc = Service {
profiles: vec!["debug".to_string(), "prod".to_string()],
..Default::default()
};
let active: HashSet<String> = ["prod".to_string()].into();
assert!(service_in_profiles(&svc, &active));
}
#[test]
fn retain_active_profiles_keeps_unprofiled_and_active() {
let yaml = "services:\n \
web:\n image: x\n \
debugger:\n image: x\n profiles: [debug]\n \
db:\n image: x\n profiles: [prod]\n";
let mut file = crate::parse_str(yaml).unwrap();
retain_active_profiles(&mut file, &["debug".to_string()]);
assert!(file.services.contains_key("web"));
assert!(file.services.contains_key("debugger"));
assert!(!file.services.contains_key("db"));
let mut file = crate::parse_str(yaml).unwrap();
temp_env::with_var_unset("COMPOSE_PROFILES", || {
retain_active_profiles(&mut file, &[]);
});
assert!(file.services.contains_key("web"));
assert_eq!(file.services.len(), 1);
}
#[test]
fn wildcard_enables_all_profiles() {
let svc = Service {
profiles: vec!["debug".to_string()],
..Default::default()
};
let active: HashSet<String> = ["*".to_string()].into();
assert!(service_in_profiles(&svc, &active));
let yaml = "services:\n \
web:\n image: x\n \
debugger:\n image: x\n profiles: [debug]\n \
db:\n image: x\n profiles: [prod]\n";
let mut file = crate::parse_str(yaml).unwrap();
retain_active_profiles(&mut file, &["*".to_string()]);
assert_eq!(file.services.len(), 3);
}
#[test]
fn implicit_activation_keeps_profiled_dependency() {
let yaml = "services:\n \
app:\n image: x\n depends_on: [db]\n \
db:\n image: x\n profiles: [storage]\n";
let mut file = crate::parse_str(yaml).unwrap();
temp_env::with_var_unset("COMPOSE_PROFILES", || {
retain_active_profiles(&mut file, &[]);
});
assert!(file.services.contains_key("app"));
assert!(
file.services.contains_key("db"),
"profiled depends_on target is implicitly activated"
);
}
#[test]
fn implicit_activation_is_transitive() {
let yaml = "services:\n \
app:\n image: x\n depends_on: [db]\n \
db:\n image: x\n profiles: [p]\n depends_on: [storage]\n \
storage:\n image: x\n profiles: [q]\n";
let mut file = crate::parse_str(yaml).unwrap();
temp_env::with_var_unset("COMPOSE_PROFILES", || {
retain_active_profiles(&mut file, &[]);
});
assert_eq!(file.services.len(), 3);
}
#[test]
fn unrelated_profiled_service_still_dropped() {
let yaml = "services:\n \
app:\n image: x\n depends_on: [db]\n \
db:\n image: x\n profiles: [storage]\n \
extra:\n image: x\n profiles: [other]\n";
let mut file = crate::parse_str(yaml).unwrap();
temp_env::with_var_unset("COMPOSE_PROFILES", || {
retain_active_profiles(&mut file, &[]);
});
assert!(file.services.contains_key("db"));
assert!(!file.services.contains_key("extra"));
}
#[test]
fn named_target_keeps_inactive_profile_service() {
let yaml = "services:\n \
web:\n image: x\n \
debugger:\n image: x\n profiles: [debug]\n";
let mut file = crate::parse_str(yaml).unwrap();
temp_env::with_var_unset("COMPOSE_PROFILES", || {
retain_active_profiles_with_targets(&mut file, &[], &["debugger".to_string()]);
});
assert!(file.services.contains_key("web"));
assert!(file.services.contains_key("debugger"));
}
}