use std::collections::{BTreeMap, HashMap};
use nojson::DisplayJson;
use crate::{
ContainerRequest, Image,
core::{
client::xpc_client::VolumeResolution, error::Result, mounts::AccessMode,
ports::ContainerPort,
},
};
pub(crate) fn normalize_platform(
platform: Option<&str>,
) -> (&'static str, bool, Option<&'static str>) {
match platform {
Some("linux/amd64" | "amd64") => ("amd64", true, Some("linux/amd64")),
Some("linux/arm64" | "arm64") => ("arm64", false, Some("linux/arm64")),
_ => ("arm64", false, None),
}
}
fn allocate_free_host_port(port: ContainerPort) -> Result<u16> {
let allocated = match port {
ContainerPort::Tcp(_) | ContainerPort::Sctp(_) => {
std::net::TcpListener::bind(("127.0.0.1", 0))
.and_then(|l| l.local_addr())
.map(|a| a.port())
}
ContainerPort::Udp(_) => std::net::UdpSocket::bind(("127.0.0.1", 0))
.and_then(|s| s.local_addr())
.map(|a| a.port()),
};
allocated.map_err(|e| crate::Error::other(format!("failed to allocate free host port: {e}")))
}
fn allocate_unique_free_host_port(
port: ContainerPort,
allocated: &mut std::collections::HashSet<u16>,
) -> Result<u16> {
for _ in 0..64 {
let candidate = allocate_free_host_port(port)?;
if allocated.insert(candidate) {
return Ok(candidate);
}
}
Err(crate::Error::other(
"failed to allocate a unique free host port after retries",
))
}
pub(crate) fn reject_sctp_ports<I: Image>(req: &ContainerRequest<I>) -> Result<()> {
let mapped_has_sctp = req.ports().is_some_and(|ps| {
ps.iter()
.any(|p| matches!(p.container_port(), ContainerPort::Sctp(_)))
});
let exposed_has_sctp = req
.expose_ports()
.iter()
.any(|p| matches!(p, ContainerPort::Sctp(_)));
if mapped_has_sctp || exposed_has_sctp {
return Err(crate::Error::other(
"SCTP port publishing is not supported on Apple container",
));
}
Ok(())
}
const SWIFT_REFERENCE_DATE_UNIX_SECS: f64 = 978_307_200.0;
fn unix_secs_to_swift_reference_date(unix_secs: f64) -> f64 {
unix_secs - SWIFT_REFERENCE_DATE_UNIX_SECS
}
pub(crate) fn build_config<I: Image>(
req: &ContainerRequest<I>,
id: &str,
desc_raw: &str,
image_config: &crate::core::client::image_config::ImageConfig,
volume_resolutions: &HashMap<String, VolumeResolution>,
) -> Result<ContainerCfg> {
let (init_exe, arguments) = image_config.effective_command(req.entrypoint(), req.cmd())?;
let env: Vec<String> = req
.env_vars()
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect::<BTreeMap<String, String>>()
.into_iter()
.map(|(k, v)| format!("{k}={v}"))
.collect();
let image_ref = crate::core::client::xpc_client::normalize_image_reference(&req.descriptor());
let cpus = 4i32;
let wd = req
.working_dir()
.map(|s| s.to_string())
.unwrap_or_else(|| "/".into());
let mounts: Vec<MountCfg> = req
.mounts()
.map(|m| mount_cfg(m, volume_resolutions))
.collect();
reject_sctp_ports(req)?;
if let Some(ports) = req.ports() {
crate::core::containers::request::reject_duplicate_mapped_ports(ports)?;
}
let mut allocated_host_ports = std::collections::HashSet::new();
let mut ports: Vec<PortCfg> = req
.ports()
.map(|ps| {
ps.iter()
.map(|p| {
let host_port = if p.host_port() == 0 {
allocate_unique_free_host_port(
p.container_port(),
&mut allocated_host_ports,
)?
} else {
if !allocated_host_ports.insert(p.host_port()) {
return Err(crate::Error::other(format!(
"duplicate host port mapping for container port {}: \
host port {} is already mapped",
p.container_port(),
p.host_port()
)));
}
p.host_port()
};
Ok(PortCfg {
host_address: "0.0.0.0".into(),
host_port,
container_port: p.container_port().as_u16(),
proto: p.container_port().as_str().into(),
})
})
.collect::<Result<Vec<_>>>()
})
.transpose()?
.unwrap_or_default();
for exposed in req.expose_ports() {
let proto = exposed.as_str();
if ports
.iter()
.any(|p| p.container_port == exposed.as_u16() && p.proto == proto)
{
continue;
}
ports.push(PortCfg {
host_address: "0.0.0.0".into(),
host_port: allocate_unique_free_host_port(*exposed, &mut allocated_host_ports)?,
container_port: exposed.as_u16(),
proto: proto.into(),
});
}
let (architecture, rosetta, _) = normalize_platform(req.platform().as_deref());
let creation_date = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| unix_secs_to_swift_reference_date(d.as_secs_f64()))
.unwrap_or(0.0);
let cap_add = if req.privileged() {
vec!["ALL".to_string()]
} else {
req.cap_add().cloned().unwrap_or_default()
};
let network = req
.network()
.clone()
.unwrap_or_else(|| "default".to_string());
Ok(ContainerCfg {
id: id.to_string(),
hostname: req
.hostname()
.map(str::to_owned)
.or_else(|| req.container_name().clone())
.unwrap_or_else(|| id.to_string()),
image_ref,
image_descriptor: desc_raw.to_string(),
mounts,
ports,
init_exe,
init_args: arguments,
init_env: env,
working_directory: wd,
use_init: req.init(),
cap_add,
cap_drop: req.cap_drop().cloned().unwrap_or_default(),
shm_size: req.shm_size(),
user: parse_user(req.user())?,
network,
architecture: architecture.to_string(),
rosetta,
ssh: req.ssh(),
read_only: req.readonly_rootfs(),
labels: req.labels().clone(),
cpus,
creation_date,
terminal: req.open_stdin().unwrap_or(false),
masked_paths: req.masked_paths().cloned(),
readonly_paths: req.readonly_paths().cloned(),
})
}
fn mount_cfg(
m: &crate::core::mounts::Mount,
volume_resolutions: &HashMap<String, VolumeResolution>,
) -> MountCfg {
let (source, fs_type, volume_name, volume_format) = match m.mount_type() {
crate::core::mounts::MountType::Bind => (
m.source().map(|s| s.to_string()).unwrap_or_default(),
"virtiofs",
None,
None,
),
crate::core::mounts::MountType::Volume => {
let name = m.source().map(|s| s.to_string()).unwrap_or_default();
let resolution = volume_resolutions.get(&name);
(
resolution.map(|r| r.source.clone()).unwrap_or_default(),
"volume",
Some(name),
resolution.map(|r| r.format.clone()),
)
}
crate::core::mounts::MountType::Tmpfs => ("tmpfs".into(), "tmpfs", None, None),
};
let destination = m.target().map(|s| s.to_string()).unwrap_or_default();
let readonly = matches!(m.access_mode(), AccessMode::ReadOnly);
let mut options = vec![if readonly {
"ro".to_string()
} else {
"rw".to_string()
}];
if matches!(m.mount_type(), crate::core::mounts::MountType::Tmpfs)
&& let Some(tmpfs) = m.tmpfs_options()
{
if let Some(size) = tmpfs.size_bytes {
options.push(format!("size={size}"));
}
if let Some(mode) = tmpfs.mode {
options.push(format!("mode={mode:o}"));
}
}
MountCfg {
source,
destination,
fs_type: fs_type.into(),
volume_name,
volume_format,
options,
}
}
pub(crate) struct ContainerCfg {
id: String,
hostname: String,
image_ref: String,
image_descriptor: String,
mounts: Vec<MountCfg>,
ports: Vec<PortCfg>,
init_exe: String,
init_args: Vec<String>,
init_env: Vec<String>,
working_directory: String,
use_init: bool,
cap_add: Vec<String>,
cap_drop: Vec<String>,
shm_size: Option<u64>,
user: ProcessUser,
network: String,
architecture: String,
rosetta: bool,
ssh: bool,
read_only: bool,
labels: std::collections::BTreeMap<String, String>,
cpus: i32,
creation_date: f64,
terminal: bool,
masked_paths: Option<Vec<String>>,
readonly_paths: Option<Vec<String>>,
}
impl DisplayJson for ContainerCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("id", &self.id)?;
f.member(
"image",
&ImageCfg {
reference: self.image_ref.clone(),
descriptor_raw: self.image_descriptor.clone(),
},
)?;
f.member("mounts", &self.mounts)?;
f.member("publishedPorts", &self.ports)?;
f.member("publishedSockets", Vec::<String>::new())?;
f.member("labels", &self.labels)?;
f.member("sysctls", HashMap::<String, String>::new())?;
f.member(
"networks",
vec![NetworkCfg {
network: self.network.clone(),
hostname: self.hostname.clone(),
}],
)?;
f.member(
"dns",
&DnsCfg {
nameservers: vec![],
options: vec![],
search_domains: vec![],
},
)?;
f.member("rosetta", self.rosetta)?;
f.member(
"initProcess",
&InitProcessCfg {
exe: self.init_exe.clone(),
args: self.init_args.clone(),
env: self.init_env.clone(),
wd: self.working_directory.clone(),
user: self.user.clone(),
terminal: self.terminal,
},
)?;
f.member(
"platform",
&ContainerPlatform {
architecture: self.architecture.clone(),
},
)?;
f.member("resources", &ResourcesCfg { cpus: self.cpus })?;
f.member("runtimeHandler", "container-runtime-linux")?;
f.member("virtualization", false)?;
f.member("ssh", self.ssh)?;
f.member("readOnly", self.read_only)?;
f.member("useInit", self.use_init)?;
f.member("capAdd", &self.cap_add)?;
f.member("capDrop", &self.cap_drop)?;
f.member("shmSize", self.shm_size)?;
f.member("maskedPaths", &self.masked_paths)?;
f.member("readonlyPaths", &self.readonly_paths)?;
f.member("stopSignal", &Option::<String>::None)?;
f.member("creationDate", self.creation_date)
})
}
}
struct ImageCfg {
reference: String,
descriptor_raw: String,
}
impl DisplayJson for ImageCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("reference", &self.reference)?;
f.member("descriptor", RawJsonStr(&self.descriptor_raw))
})
}
}
struct RawJsonStr<'a>(&'a str);
impl DisplayJson for RawJsonStr<'_> {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
write!(f.inner_mut(), "{}", self.0)
}
}
struct MountCfg {
source: String,
destination: String,
fs_type: String,
volume_name: Option<String>,
volume_format: Option<String>,
options: Vec<String>,
}
impl DisplayJson for MountCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
match self.fs_type.as_str() {
"virtiofs" => f.member("type", &FsTypeObj::Virtiofs)?,
"volume" => f.member(
"type",
&FsTypeObj::Volume {
name: self.volume_name.clone().unwrap_or_default(),
format: self.volume_format.clone().unwrap_or_default(),
},
)?,
_ => f.member("type", &FsTypeObj::Tmpfs)?,
};
f.member("source", &self.source)?;
f.member("destination", &self.destination)?;
f.member("options", &self.options)
})
}
}
enum FsTypeObj {
Virtiofs,
Volume { name: String, format: String },
Tmpfs,
}
impl DisplayJson for FsTypeObj {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
match self {
FsTypeObj::Virtiofs => f.object(|f| f.member("virtiofs", &EmptyObj)),
FsTypeObj::Volume { name, format } => f.object(|f| {
f.member(
"volume",
&VolType {
name: name.clone(),
format: format.clone(),
},
)
}),
FsTypeObj::Tmpfs => f.object(|f| f.member("tmpfs", &EmptyObj)),
}
}
}
struct EmptyObj;
impl DisplayJson for EmptyObj {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|_| Ok(()))
}
}
struct KeyedEmptyObj {
key: &'static str,
}
impl DisplayJson for KeyedEmptyObj {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| f.member(self.key, &EmptyObj))
}
}
struct VolType {
name: String,
format: String,
}
impl DisplayJson for VolType {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("name", &self.name)?;
f.member("format", &self.format)?;
f.member("cache", &KeyedEmptyObj { key: "auto" })?;
f.member("sync", &KeyedEmptyObj { key: "fsync" })
})
}
}
struct PortCfg {
host_address: String,
host_port: u16,
container_port: u16,
proto: String,
}
impl DisplayJson for PortCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("hostAddress", &self.host_address)?;
f.member("hostPort", self.host_port)?;
f.member("containerPort", self.container_port)?;
f.member("proto", &self.proto)?;
f.member("count", 1u16)
})
}
}
struct InitProcessCfg {
exe: String,
args: Vec<String>,
env: Vec<String>,
wd: String,
user: ProcessUser,
terminal: bool,
}
impl DisplayJson for InitProcessCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("executable", &self.exe)?;
f.member("arguments", &self.args)?;
f.member("environment", &self.env)?;
f.member("workingDirectory", &self.wd)?;
f.member("terminal", self.terminal)?;
f.member("user", &self.user)?;
f.member("supplementalGroups", Vec::<u32>::new())?;
f.member("rlimits", Vec::<u32>::new())
})
}
}
#[derive(Clone, Debug)]
enum ProcessUser {
Id { uid: u32, gid: u32 },
Raw { user_string: String },
}
impl DisplayJson for ProcessUser {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
match self {
ProcessUser::Id { uid, gid } => f.object(|f| {
f.member(
"id",
&ProcessUserId {
uid: *uid,
gid: *gid,
},
)
}),
ProcessUser::Raw { user_string } => {
f.object(|f| f.member("raw", &ProcessUserRaw { user_string }))
}
}
}
}
struct ProcessUserId {
uid: u32,
gid: u32,
}
impl DisplayJson for ProcessUserId {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("uid", self.uid)?;
f.member("gid", self.gid)
})
}
}
struct ProcessUserRaw<'a> {
user_string: &'a str,
}
impl DisplayJson for ProcessUserRaw<'_> {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| f.member("userString", self.user_string))
}
}
fn parse_user(user: Option<&str>) -> Result<ProcessUser> {
let Some(user) = user else {
return Ok(ProcessUser::Id { uid: 0, gid: 0 });
};
if user.is_empty() {
return Ok(ProcessUser::Id { uid: 0, gid: 0 });
}
let parts: Vec<&str> = user.splitn(2, ':').collect();
let uid_str = parts[0];
let gid_str = parts.get(1).copied();
if let Ok(uid) = uid_str.parse::<u32>() {
let gid = match gid_str {
Some(s) => s
.parse::<u32>()
.map_err(|_| crate::Error::other(format!("invalid gid in user string: {user}")))?,
None => 0,
};
return Ok(ProcessUser::Id { uid, gid });
}
Ok(ProcessUser::Raw {
user_string: user.to_string(),
})
}
struct ContainerPlatform {
architecture: String,
}
impl DisplayJson for ContainerPlatform {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("os", "linux")?;
f.member("architecture", &self.architecture)?;
f.member("variant", &Option::<String>::None)
})
}
}
struct ResourcesCfg {
cpus: i32,
}
impl DisplayJson for ResourcesCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("cpus", self.cpus)?;
f.member("memoryInBytes", 1024u64 * 1024 * 1024)?;
f.member("storage", Option::<u64>::None)?;
f.member("cpuOverhead", 1i32)
})
}
}
struct NetworkCfg {
network: String,
hostname: String,
}
impl DisplayJson for NetworkCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("network", &self.network)?;
f.member(
"options",
&NetOpts {
hostname: self.hostname.clone(),
mtu: 1280,
},
)
})
}
}
struct NetOpts {
hostname: String,
mtu: u32,
}
impl DisplayJson for NetOpts {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("hostname", &self.hostname)?;
f.member("mtu", self.mtu)
})
}
}
struct DnsCfg {
nameservers: Vec<String>,
options: Vec<String>,
search_domains: Vec<String>,
}
impl DisplayJson for DnsCfg {
fn fmt(&self, f: &mut nojson::JsonFormatter) -> std::fmt::Result {
f.object(|f| {
f.member("nameservers", &self.nameservers)?;
f.member("options", &self.options)?;
f.member("searchDomains", &self.search_domains)
})
}
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use std::borrow::Cow;
use crate::{ContainerRequest, GenericImage, ImageExt, xpc::j};
use super::*;
#[test]
fn shm_size_is_reflected_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_shm_size(2 * 1024 * 1024 * 1024);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.shm_size, Some(2 * 1024 * 1024 * 1024));
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"shmSize\":2147483648"));
}
#[test]
fn shm_size_is_null_by_default_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.shm_size, None);
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"shmSize\":null"));
}
#[test]
fn masked_readonly_paths_are_null_by_default_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.masked_paths, None);
assert_eq!(cfg.readonly_paths, None);
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"maskedPaths\":null"));
assert!(json.contains("\"readonlyPaths\":null"));
}
#[test]
fn masked_readonly_paths_are_reflected_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_masked_paths(std::iter::empty::<String>())
.with_readonly_paths(["/etc", "/proc"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.masked_paths, Some(vec![]));
assert_eq!(
cfg.readonly_paths,
Some(vec!["/etc".to_string(), "/proc".to_string()])
);
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"maskedPaths\":[]"));
assert!(json.contains("\"readonlyPaths\":[\"/etc\",\"/proc\"]"));
}
#[test]
fn masked_readonly_paths_setters_overwrite_on_repeat_calls() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_masked_paths(["/a"])
.with_masked_paths(["/b", "/c"]);
assert_eq!(
req.masked_paths(),
Some(&vec!["/b".to_string(), "/c".to_string()])
);
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_readonly_paths(["/a"])
.with_readonly_paths(std::iter::empty::<String>());
assert_eq!(req.readonly_paths(), Some(&vec![]));
}
#[test]
fn masked_paths_explicit_list_is_emitted_as_array() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_masked_paths(["/tmp/sensitive", "/proc/kcore"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"maskedPaths\":[\"/tmp/sensitive\",\"/proc/kcore\"]"));
}
#[test]
fn user_numeric_is_reflected_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_user("1000:1000");
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"user\":{\"id\":{\"uid\":1000,\"gid\":1000}}"));
}
#[test]
fn network_is_reflected_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_network("my-net");
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.network, "my-net");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"networks\":[{\"network\":\"my-net\""));
}
#[test]
fn network_is_default_when_unspecified_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.network, "default");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"networks\":[{\"network\":\"default\""));
}
#[test]
fn hostname_is_reflected_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_hostname("guest-host");
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.hostname, "guest-host");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"hostname\":\"guest-host\""),
"JSON に指定ホスト名が含まれること: {json}"
);
assert!(
!json.contains("\"hostname\":\"test-id\""),
"フォールバックの id が残っていないこと: {json}"
);
}
#[test]
fn hostname_falls_back_to_container_name_when_unspecified() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_container_name("named-box");
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.hostname, "named-box");
}
#[test]
fn hostname_falls_back_to_id_when_name_unspecified() {
let req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"generated-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.hostname, "generated-id");
}
#[test]
fn hostname_wins_over_container_name_when_both_set() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_container_name("container-name-value")
.with_hostname("hostname-value");
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.hostname, "hostname-value");
assert_eq!(req.hostname(), Some("hostname-value"));
assert_eq!(
req.container_name().as_deref(),
Some("container-name-value")
);
}
#[test]
fn readonly_rootfs_true_is_reflected_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_readonly_rootfs(true);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(cfg.read_only);
assert!(req.readonly_rootfs());
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"readOnly\":true"),
"JSON に readOnly:true が含まれること: {json}"
);
}
#[test]
fn readonly_rootfs_defaults_to_false_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(!cfg.read_only);
assert!(!req.readonly_rootfs());
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"readOnly\":false"),
"JSON に readOnly:false が含まれること: {json}"
);
}
#[test]
fn open_stdin_true_sets_terminal_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_open_stdin(true);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(cfg.terminal);
assert_eq!(req.open_stdin(), Some(true));
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"terminal\":true"),
"JSON に terminal:true が含まれること: {json}"
);
}
#[test]
fn open_stdin_defaults_to_terminal_false() {
let req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(!cfg.terminal);
assert_eq!(req.open_stdin(), None);
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"terminal\":false"),
"JSON に terminal:false が含まれること: {json}"
);
}
#[test]
fn entrypoint_receives_whole_cmd_as_arguments() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_entrypoint("/entry.sh")
.with_cmd(["serve", "--port", "80"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.init_exe, "/entry.sh");
assert_eq!(cfg.init_args, vec!["serve", "--port", "80"]);
}
#[test]
fn missing_cmd_and_entrypoint_is_an_error() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest").into();
let result = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
);
assert!(result.is_err(), "空のコマンドはエラーになること");
}
#[test]
fn image_config_fallback_resolves_default_cmd() {
let req: ContainerRequest<GenericImage> = GenericImage::new("nginx", "latest").into();
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig {
entrypoint: None,
cmd: Some(vec!["nginx".into(), "-g".into(), "daemon off;".into()]),
},
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.init_exe, "nginx");
assert_eq!(cfg.init_args, vec!["-g", "daemon off;"]);
}
#[test]
fn user_cmd_overrides_image_config_cmd() {
let req: ContainerRequest<GenericImage> =
GenericImage::new("nginx", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig {
entrypoint: None,
cmd: Some(vec!["nginx".into()]),
},
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.init_exe, "sleep");
assert_eq!(cfg.init_args, vec!["1"]);
}
#[test]
fn user_entrypoint_overrides_image_config_entrypoint() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_entrypoint("/bin/bash")
.into();
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig {
entrypoint: Some(vec!["/bin/sh".into(), "-c".into()]),
cmd: Some(vec!["echo".into(), "image".into()]),
},
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.init_exe, "/bin/bash");
assert_eq!(cfg.init_args, vec!["echo", "image"]);
}
#[test]
fn exposed_ports_get_auto_allocated_host_ports() {
use crate::core::ports::IntoContainerPort;
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_exposed_port(80.tcp())
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.ports.len(), 1);
assert_eq!(cfg.ports[0].container_port, 80);
assert_ne!(
cfg.ports[0].host_port, 0,
"ホストポートが割り当てられること"
);
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_exposed_port(80.tcp())
.with_mapped_port(18080, 80.tcp())
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.ports.len(), 1);
assert_eq!(cfg.ports[0].host_port, 18080);
}
#[test]
fn multiple_exposed_ports_get_unique_host_ports() {
use crate::core::ports::IntoContainerPort;
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_exposed_port(80.tcp())
.with_exposed_port(81.tcp())
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.ports.len(), 2);
assert_ne!(
cfg.ports[0].host_port, cfg.ports[1].host_port,
"複数の expose が異なるホストポートを割り当てられること"
);
}
#[test]
fn mapped_port_zero_gets_auto_allocated_host_port() {
use crate::core::ports::IntoContainerPort;
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mapped_port(0, 80.tcp())
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.ports.len(), 1);
assert_eq!(cfg.ports[0].container_port, 80);
assert_ne!(
cfg.ports[0].host_port, 0,
"ホストポート 0 が自動割当されること"
);
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_exposed_port(80.tcp())
.with_mapped_port(0, 80.tcp())
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.ports.len(), 1, "重複する expose はスキップされること");
assert_ne!(cfg.ports[0].host_port, 0);
let mut req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
for port in 80_u16..=143 {
req = req.with_mapped_port(0, port.tcp());
}
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.ports.len(), 64);
let mut seen = std::collections::HashSet::new();
for p in &cfg.ports {
assert_ne!(p.host_port, 0);
assert!(
seen.insert(p.host_port),
"割当ホストポートが重複しないこと (host_port={})",
p.host_port
);
}
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mapped_port(0, 53.udp())
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(cfg.ports.len(), 1);
assert_eq!(cfg.ports[0].container_port, 53);
assert_ne!(
cfg.ports[0].host_port, 0,
"UDP の mapped(0) にもホストポートが割り当てられること"
);
}
#[test]
fn duplicate_fixed_host_ports_are_rejected() {
use crate::core::ports::IntoContainerPort;
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mapped_port(18080, 80.tcp())
.with_mapped_port(18080, 81.tcp())
.with_cmd(["sleep", "1"]);
let result = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
);
let err = match result {
Ok(_) => panic!("重複するホストポートのマッピングはエラーになること"),
Err(e) => e,
};
assert!(
err.to_string().contains("duplicate host port mapping"),
"重複ホストポートのエラーメッセージであること: {err}"
);
}
#[test]
fn duplicate_container_ports_are_rejected_by_build_config() {
use crate::core::ports::IntoContainerPort;
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mapped_port(18080, 80.tcp())
.with_mapped_port(18081, 80.tcp())
.with_cmd(["sleep", "1"]);
let result = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
);
let err = match result {
Ok(_) => panic!("重複コンテナポートのマッピングはエラーになること"),
Err(e) => e,
};
assert!(
err.to_string().contains("duplicate container port mapping"),
"重複コンテナポートのエラーメッセージであること: {err}"
);
}
#[test]
fn sctp_exposed_port_is_rejected_by_build_config() {
use crate::core::ports::IntoContainerPort;
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_exposed_port(5060.sctp())
.with_cmd(["sleep", "1"]);
let result = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
);
let err = match result {
Err(e) => e,
Ok(_) => panic!("SCTP exposed port は拒否されること"),
};
assert!(
err.to_string()
.contains("SCTP port publishing is not supported on Apple container"),
"エラーメッセージが SCTP 未対応を明示すること: {err}"
);
}
#[test]
fn sctp_mapped_port_is_rejected_by_build_config() {
use crate::core::ports::IntoContainerPort;
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mapped_port(15060, 5060.sctp())
.with_cmd(["sleep", "1"]);
let result = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
);
let err = match result {
Err(e) => e,
Ok(_) => panic!("SCTP mapped port は拒否されること"),
};
assert!(
err.to_string()
.contains("SCTP port publishing is not supported on Apple container"),
"エラーメッセージが SCTP 未対応を明示すること: {err}"
);
}
#[test]
fn normalize_platform_maps_allowed_values() {
assert_eq!(
normalize_platform(Some("linux/amd64")),
("amd64", true, Some("linux/amd64"))
);
assert_eq!(
normalize_platform(Some("amd64")),
("amd64", true, Some("linux/amd64"))
);
assert_eq!(
normalize_platform(Some("linux/arm64")),
("arm64", false, Some("linux/arm64"))
);
assert_eq!(
normalize_platform(Some("arm64")),
("arm64", false, Some("linux/arm64"))
);
assert_eq!(normalize_platform(None), ("arm64", false, None));
assert_eq!(
normalize_platform(Some("linux/ppc64le")),
("arm64", false, None)
);
assert_eq!(normalize_platform(Some("x86_64")), ("arm64", false, None));
}
#[test]
fn with_platform_amd64_enables_rosetta() {
for platform in ["linux/amd64", "amd64"] {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_platform(platform)
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(
cfg.rosetta,
"platform {platform} が Rosetta を有効にすること"
);
assert_eq!(
cfg.architecture, "amd64",
"platform {platform} が architecture を amd64 に設定すること"
);
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains(r#""architecture":"amd64""#),
"platform JSON が {platform} の amd64 を含むこと: {json}"
);
}
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_platform("linux/arm64")
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(!cfg.rosetta);
assert_eq!(cfg.architecture, "arm64");
let req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(!cfg.rosetta);
assert_eq!(cfg.architecture, "arm64");
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_platform("linux/ppc64le")
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert!(!cfg.rosetta);
assert_eq!(cfg.architecture, "arm64");
}
#[test]
fn volume_mount_uses_apple_container_enum_json_format() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mount(crate::core::mounts::Mount::volume_mount(
"data-volume",
"/data",
))
.with_cmd(["sleep", "1"]);
let resolutions = HashMap::from([(
"data-volume".to_string(),
VolumeResolution {
source: "/host/volumes/data-volume/volume.img".into(),
format: "ext4".into(),
},
)]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&resolutions,
)
.expect("コンテナ設定の構築に成功すること");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"name\":\"data-volume\""),
"volume 名が文字列のままであること: {json}"
);
assert!(
json.contains("\"format\":\"ext4\""),
"format が解決済みの値のままであること: {json}"
);
assert!(
json.contains("\"source\":\"/host/volumes/data-volume/volume.img\""),
"source が解決済みの実パスであること: {json}"
);
assert!(
json.contains("\"cache\":{\"auto\":{}}"),
"cache が単一キーオブジェクト形式であること: {json}"
);
assert!(
json.contains("\"sync\":{\"fsync\":{}}"),
"sync が単一キーオブジェクト形式であること: {json}"
);
assert!(
!json.contains("\"cache\":\"auto\""),
"cache が文字列形式のままでないこと: {json}"
);
}
#[test]
fn volume_mount_without_resolution_emits_empty_source() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mount(crate::core::mounts::Mount::volume_mount(
"unresolved-volume",
"/data",
))
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("コンテナ設定の構築に成功すること");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"source\":\"\""),
"未解決 volume の source が空であること: {json}"
);
assert!(
!json.contains("\"source\":\"unresolved-volume\""),
"未解決 volume がボリューム名のまま送られないこと: {json}"
);
}
#[test]
fn mounts_are_mapped_to_xpc_payload_configuration() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mount(crate::core::mounts::Mount::bind_mount(
"/host/bind",
"/container/bind",
))
.with_mount(
crate::core::mounts::Mount::volume_mount("data-volume", "/container/volume")
.with_access_mode(crate::core::mounts::AccessMode::ReadOnly),
)
.with_mount(crate::core::mounts::Mount::tmpfs_mount("/container/tmpfs"))
.with_cmd(["sleep", "1"]);
let resolutions = HashMap::from([(
"data-volume".to_string(),
VolumeResolution {
source: "/host/volumes/data-volume/volume.img".into(),
format: "ext4".into(),
},
)]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&resolutions,
)
.expect("コンテナ設定の構築に成功すること");
assert_eq!(cfg.mounts.len(), 3);
assert_eq!(cfg.mounts[0].source, "/host/bind");
assert_eq!(cfg.mounts[0].destination, "/container/bind");
assert_eq!(cfg.mounts[0].fs_type, "virtiofs");
assert_eq!(cfg.mounts[0].volume_name, None);
assert_eq!(cfg.mounts[0].options, vec!["rw".to_string()]);
assert_eq!(
cfg.mounts[1].source, "/host/volumes/data-volume/volume.img",
"volume の source が解決済みの実パスであること"
);
assert_eq!(cfg.mounts[1].fs_type, "volume");
assert_eq!(cfg.mounts[1].volume_name.as_deref(), Some("data-volume"));
assert_eq!(
cfg.mounts[1].volume_format.as_deref(),
Some("ext4"),
"volume の format が解決済みの値であること"
);
assert_eq!(cfg.mounts[1].options, vec!["ro".to_string()]);
assert_eq!(cfg.mounts[2].source, "tmpfs");
assert_eq!(cfg.mounts[2].fs_type, "tmpfs");
assert_eq!(cfg.mounts[2].volume_name, None);
assert_eq!(cfg.mounts[2].options, vec!["rw".to_string()]);
}
#[test]
fn tmpfs_size_and_mode_are_reflected_in_mount_options() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_mount(
crate::core::mounts::Mount::tmpfs_mount("/container/tmpfs")
.with_size_bytes(64 * 1024 * 1024)
.with_mode(0o1777),
)
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("コンテナ設定の構築に成功すること");
assert_eq!(cfg.mounts.len(), 1);
assert_eq!(
cfg.mounts[0].options,
vec![
"rw".to_string(),
"size=67108864".to_string(),
"mode=1777".to_string(),
]
);
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(
json.contains("\"size=67108864\""),
"JSON に size オプションが含まれること: {json}"
);
assert!(
json.contains("\"mode=1777\""),
"JSON に mode オプションが含まれること: {json}"
);
}
#[test]
fn capabilities_and_working_directory_are_mapped_to_container_cfg() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cap_add("NET_ADMIN")
.with_cap_drop("MKNOD")
.with_working_dir("/work")
.with_cmd(["sleep", "1"]);
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("コンテナ設定の構築に成功すること");
assert_eq!(cfg.cap_add, vec!["NET_ADMIN"]);
assert_eq!(cfg.cap_drop, vec!["MKNOD"]);
assert_eq!(cfg.working_directory, "/work");
let privileged_req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cap_add("NET_ADMIN")
.with_privileged(true)
.with_cmd(["sleep", "1"]);
let privileged_cfg = build_config(
&privileged_req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("特権コンテナ設定の構築に成功すること");
assert_eq!(privileged_cfg.cap_add, vec!["ALL"]);
let default_req: ContainerRequest<GenericImage> =
GenericImage::new("alpine", "latest").with_cmd(["sleep", "1"]);
let default_cfg = build_config(
&default_req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("既定のコンテナ設定の構築に成功すること");
assert_eq!(default_cfg.working_directory, "/");
}
#[test]
fn unix_secs_to_swift_reference_date_subtracts_offset() {
assert_eq!(unix_secs_to_swift_reference_date(978_307_200.0), 0.0);
assert_eq!(
unix_secs_to_swift_reference_date(1_783_170_588.0),
1_783_170_588.0 - 978_307_200.0
);
}
#[test]
fn user_name_is_raw_in_container_cfg_json() {
let req: ContainerRequest<GenericImage> = GenericImage::new("alpine", "latest")
.with_cmd(["sleep", "1"])
.with_user("nobody");
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
let json = String::from_utf8(j(&cfg)).expect("設定が有効な UTF-8 JSON であること");
assert!(json.contains("\"user\":{\"raw\":{\"userString\":\"nobody\"}}"));
}
struct DefaultEnvImage;
impl Image for DefaultEnvImage {
fn name(&self) -> &str {
"alpine"
}
fn tag(&self) -> &str {
"latest"
}
fn ready_conditions(&self) -> Vec<crate::core::WaitFor> {
Vec::new()
}
fn env_vars(
&self,
) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
[("FOO", "from_image"), ("IMAGE_ONLY", "from_image_only")]
}
}
#[test]
fn env_vars_are_folded_with_request_winning_in_init_env() {
let req: ContainerRequest<DefaultEnvImage> = ContainerRequest::from(DefaultEnvImage)
.with_cmd(["sleep", "1"])
.with_env_var("FOO", "from_request")
.with_env_var("REQ_ONLY", "from_request_only");
let cfg = build_config(
&req,
"test-id",
"{}",
&crate::core::client::image_config::ImageConfig::default(),
&HashMap::new(),
)
.expect("build_config が成功すること");
assert_eq!(
cfg.init_env,
vec![
"FOO=from_request".to_string(),
"IMAGE_ONLY=from_image_only".to_string(),
"REQ_ONLY=from_request_only".to_string(),
]
);
}
}