use std::collections::HashMap;
use bollard::models::{
ContainerConfig, ContainerCreateBody, ContainerInspectResponse, EndpointSettings, HostConfig,
NetworkingConfig,
};
use bollard::query_parameters::{CreateContainerOptions, CreateContainerOptionsBuilder};
#[derive(Debug, thiserror::Error)]
pub enum SpecError {
#[error("inspect response is missing the {0} field")]
Missing(&'static str),
}
#[derive(Debug, Clone)]
pub struct ContainerSpec {
pub name: String,
pub image_ref: String,
pub image_id: Option<String>,
pub config: ContainerConfig,
pub host_config: Option<HostConfig>,
pub network_endpoints: Option<HashMap<String, EndpointSettings>>,
}
impl ContainerSpec {
pub fn from_inspect(resp: ContainerInspectResponse) -> Result<Self, SpecError> {
let name = resp
.name
.as_deref()
.ok_or(SpecError::Missing("Name"))?
.trim_start_matches('/')
.to_owned();
let config = resp.config.ok_or(SpecError::Missing("Config"))?;
let image_ref = config
.image
.clone()
.ok_or(SpecError::Missing("Config.Image"))?;
let network_endpoints = resp.network_settings.and_then(|ns| ns.networks);
Ok(Self {
name,
image_ref,
image_id: resp.image,
config,
host_config: resp.host_config,
network_endpoints,
})
}
pub fn to_create_body(&self, new_image: &str) -> ContainerCreateBody {
let cfg = &self.config;
ContainerCreateBody {
hostname: cfg.hostname.clone(),
domainname: cfg.domainname.clone(),
user: cfg.user.clone(),
attach_stdin: cfg.attach_stdin,
attach_stdout: cfg.attach_stdout,
attach_stderr: cfg.attach_stderr,
exposed_ports: cfg.exposed_ports.clone(),
tty: cfg.tty,
open_stdin: cfg.open_stdin,
stdin_once: cfg.stdin_once,
env: cfg.env.clone(),
cmd: cfg.cmd.clone(),
healthcheck: cfg.healthcheck.clone(),
args_escaped: cfg.args_escaped,
image: Some(new_image.to_owned()),
volumes: cfg.volumes.clone(),
working_dir: cfg.working_dir.clone(),
entrypoint: cfg.entrypoint.clone(),
network_disabled: cfg.network_disabled,
on_build: cfg.on_build.clone(),
labels: cfg.labels.clone(),
stop_signal: cfg.stop_signal.clone(),
stop_timeout: cfg.stop_timeout,
shell: cfg.shell.clone(),
host_config: self.host_config.clone(),
networking_config: self.network_endpoints.clone().map(|e| NetworkingConfig {
endpoints_config: Some(e),
}),
}
}
pub fn to_create_options(&self) -> CreateContainerOptions {
CreateContainerOptionsBuilder::new()
.name(&self.name)
.build()
}
}
pub fn split_labels(
labels: &HashMap<String, String>,
) -> (HashMap<String, String>, HashMap<String, String>) {
let mut managed = HashMap::new();
let mut user = HashMap::new();
for (k, v) in labels {
if k.starts_with("freshdock.") {
managed.insert(k.clone(), v.clone());
} else {
user.insert(k.clone(), v.clone());
}
}
(managed, user)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn split_labels_partitions_freshdock_prefix_from_user_labels() {
let labels: HashMap<String, String> = [
("freshdock.enable", "true"),
("freshdock.mode", "watch"),
("app", "demo"),
("team", "infra"),
]
.iter()
.map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
.collect();
let (managed, user) = split_labels(&labels);
assert_eq!(managed.len(), 2);
assert_eq!(
managed.get("freshdock.enable").map(String::as_str),
Some("true")
);
assert_eq!(
managed.get("freshdock.mode").map(String::as_str),
Some("watch")
);
assert_eq!(user.len(), 2);
assert_eq!(user.get("app").map(String::as_str), Some("demo"));
assert_eq!(user.get("team").map(String::as_str), Some("infra"));
assert!(
!user.contains_key("freshdock.enable"),
"managed labels must not leak into the user partition"
);
}
#[test]
fn split_labels_handles_empty_input() {
let (managed, user) = split_labels(&HashMap::new());
assert!(managed.is_empty());
assert!(user.is_empty());
}
}