///|
/// Volume mounts and network wiring: the two places where Compose's shorthand
/// syntax has to be unpacked before podman sees it.
///|
/// Every mount of a container, parsed once so the args builder and the
/// volume-creation pass share the same work.
pub fn container_mounts(compose : Compose, cnt : @yaml.Yaml) -> Array[Mount] {
let out : Array[Mount] = []
for volume in y_list(y_get(cnt, "volumes").unwrap_or(y_null())) {
out.push(parse_mount(compose, cnt, volume))
}
out
}
///|
/// Translate one parsed mount into podman arguments.
pub fn mount_arg(
compose : Compose,
cnt : @yaml.Yaml,
mount : Mount,
) -> Array[String] {
let args : Array[String] = []
match mount.kind {
"bind" =>
match mount.source {
Some(source) => {
args.push("-v")
args.push(volume_spec(source, mount.target, mount.options))
}
None => ()
}
"volume" => {
let source = match mount.source {
Some(s) => resolve_volume_name(compose, cnt, s, mount.target)
None => anonymous_volume_name(compose, cnt, mount.target)
}
args.push("--mount")
args.push(
"type=volume,source=" +
source +
",destination=" +
mount.target +
opts_suffix(mount.options),
)
}
"tmpfs" => {
args.push("--tmpfs")
args.push(mount.target + tmpfs_suffix(mount.options))
}
_ => {
// image, glob and anything else go through --mount.
let source = mount.source.unwrap_or("")
args.push("--mount")
args.push(
"type=" +
mount.kind +
",source=" +
source +
",destination=" +
mount.target +
opts_suffix(mount.options),
)
}
}
args
}
///|
/// `source:target` plus a comma-joined option list.
fn volume_spec(
source : String,
target : String,
options : Array[String],
) -> String {
if options.length() == 0 {
source + ":" + target
} else {
source + ":" + target + ":" + options.join(",")
}
}
///|
/// `,a,b` or the empty string.
fn opts_suffix(options : Array[String]) -> String {
if options.length() == 0 {
""
} else {
"," + options.join(",")
}
}
///|
/// `:size=...,mode=...` for a tmpfs mount.
fn tmpfs_suffix(options : Array[String]) -> String {
if options.length() == 0 {
""
} else {
":" + options.join(",")
}
}
///|
/// Parse a short `SOURCE:TARGET[:OPTIONS]` string or a long mapping.
fn parse_mount(
compose : Compose,
cnt : @yaml.Yaml,
volume : @yaml.Yaml,
) -> Mount {
match volume {
@yaml.Yaml::String(spec) => parse_short_mount(spec, compose.dirname)
@yaml.Yaml::Map(m) => {
let kind = match m.get("type") {
Some(t) => y_str_or(t, "volume")
None => "volume"
}
let source = match m.get("source") {
Some(s) => y_str(s)
None => None
}
let target = match m.get("target") {
Some(t) => y_str_or(t, "")
None => ""
}
let options : Array[String] = []
if y_bool(m.get("read_only").unwrap_or(y_null())).unwrap_or(false) {
options.push("ro")
}
match m.get("bind") {
Some(bind) =>
match y_map(bind) {
Some(bm) => {
match bm.get("propagation") {
Some(p) =>
match y_str(p) {
Some(s) if s.length() > 0 => options.push(s)
_ => ()
}
None => ()
}
match bm.get("selinux") {
Some(p) =>
match y_str(p) {
Some(s) if s.length() > 0 => options.push(s)
_ => ()
}
None => ()
}
if y_bool(bm.get("create_host_path").unwrap_or(y_null())).unwrap_or(
false,
) {
ignore(cnt)
}
}
None => ()
}
None => ()
}
match m.get("tmpfs") {
Some(tmpfs) =>
match y_map(tmpfs) {
Some(tm) => {
match tm.get("size") {
Some(size) =>
match y_str(size) {
Some(s) => options.push("size=" + s)
None => ()
}
None => ()
}
match tm.get("mode") {
Some(mode) =>
match y_str(mode) {
Some(s) => options.push("mode=" + s)
None => ()
}
None => ()
}
}
None => ()
}
None => ()
}
{ kind, source, target, options, }
}
_ => { kind: "volume", source: None, target: "", options: [], }
}
}
///|
/// Parse the short mount form. A bare path is an anonymous volume; a path-like
/// source is a bind mount; anything else is a named volume.
pub fn parse_short_mount(spec : String, basedir : String) -> Mount {
let parts = spec.split(":").to_array()
let mut source : String? = None
let mut target = spec
let mut opts_text = ""
match parts.length() {
1 => target = spec
2 =>
if parts[1].has_prefix("/") {
source = Some(parts[0].to_owned())
target = parts[1].to_owned()
} else {
target = parts[0].to_owned()
opts_text = parts[1].to_owned()
}
_ => {
source = Some(parts[0].to_owned())
target = parts[1].to_owned()
opts_text = parts[2].to_owned()
}
}
let options : Array[String] = []
for opt in opts_text.split(",") {
let o = opt.to_owned()
if o.length() > 0 {
options.push(o)
}
}
match source {
Some(src) =>
if is_path_like(src) {
{
kind: "bind",
source: Some(path_abspath(basedir, src)),
target,
options,
}
} else {
{ kind: "volume", source: Some(src), target, options, }
}
None => { kind: "volume", source: None, target, options, }
}
}
///|
/// Whether a mount source names a host path rather than a named volume.
fn is_path_like(source : String) -> Bool {
source.has_prefix("/") ||
source.has_prefix("./") ||
source.has_prefix("../") ||
source.has_prefix("~")
}
///|
/// The podman name of a named volume: `name`, the external name, or the
/// project-prefixed default.
pub fn resolve_volume_name(
compose : Compose,
cnt : @yaml.Yaml,
source : String,
target : String,
) -> String {
ignore(cnt)
ignore(target)
match compose.vols.get(source) {
Some(vol) =>
match y_get(vol, "name") {
Some(n) =>
match y_str(n) {
Some(s) if s.length() > 0 => s
_ => project_volume_name(compose, source, vol)
}
None => project_volume_name(compose, source, vol)
}
None => compose.project_name + "_" + source
}
}
///|
fn project_volume_name(
compose : Compose,
source : String,
vol : @yaml.Yaml,
) -> String {
match y_get(vol, "external") {
Some(ext) =>
match ext {
@yaml.Yaml::Map(m) =>
match m.get("name") {
Some(n) => y_str_or(n, source)
None => source
}
@yaml.Yaml::Boolean(b) =>
if b {
source
} else {
compose.project_name + "_" + source
}
_ => compose.project_name + "_" + source
}
None => compose.project_name + "_" + source
}
}
///|
/// A deterministic name for an anonymous volume.
fn anonymous_volume_name(
compose : Compose,
cnt : @yaml.Yaml,
target : String,
) -> String {
let service = y_str_or(y_get(cnt, "_service").unwrap_or(y_null()), "service")
compose.project_name + "_" + service + "_" + simple_hash(target)
}
///|
/// `--network` arguments for a container.
pub fn network_args(compose : Compose, cnt : @yaml.Yaml) -> Array[String] {
let args : Array[String] = []
match y_get(cnt, "network_mode") {
Some(mode) =>
match y_str(mode) {
Some(m) =>
if m.has_prefix("service:") {
let other = m[8:].to_owned()
match compose.container_names_by_service.get(other) {
Some(names) =>
if names.length() > 0 {
args.push("--network=container:" + names[0])
}
None => args.push("--network=" + m)
}
} else {
args.push("--network=" + m)
}
None => ()
}
None => {
let service_name = y_str_or(
y_get(cnt, "service_name").unwrap_or(y_null()),
"",
)
let aliases : Array[String] = ["alias=" + service_name]
let nets = container_networks(compose, cnt)
if nets.is_empty() {
// No default network: fall back to podman's own bridge.
args.push("--network=bridge:" + aliases.join(","))
} else {
for entry in nets {
let (net, config) = entry
let net_name = network_name(compose, net)
let options : Array[String] = []
options.append(aliases)
match y_map(config) {
Some(cm) => {
match cm.get("ipv4_address") {
Some(ip) =>
match y_str(ip) {
Some(s) if s.length() > 0 => options.push("ip=" + s)
_ => ()
}
None => ()
}
match cm.get("ipv6_address") {
Some(ip) =>
match y_str(ip) {
Some(s) if s.length() > 0 => options.push("ip6=" + s)
_ => ()
}
None => ()
}
match cm.get("mac_address") {
Some(mac) =>
match y_str(mac) {
Some(s) if s.length() > 0 => options.push("mac=" + s)
_ => ()
}
None => ()
}
for net_alias in y_list_str(cm.get("aliases").unwrap_or(y_null())) {
options.push("alias=" + net_alias)
}
}
None => ()
}
args.push("--network=" + net_name + ":" + options.join(","))
}
}
}
}
args
}
///|
/// The podman name of a top-level network, honouring `name` and `external`.
pub fn network_name(compose : Compose, net : String) -> String {
let desc = match compose.networks.get(net) {
Some(d) => d
None => return default_network_name(compose, net)
}
match y_get(desc, "name") {
Some(n) =>
match y_str(n) {
Some(s) if s.length() > 0 => return s
_ => ()
}
None => ()
}
match y_get(desc, "external") {
Some(ext) =>
match ext {
@yaml.Yaml::Map(m) =>
match m.get("name") {
Some(n) => return y_str_or(n, net)
None => ()
}
@yaml.Yaml::Boolean(b) => if b { return net }
_ => ()
}
None => ()
}
default_network_name(compose, net)
}
///|
/// `_` (or the compatibility spelling).
fn default_network_name(compose : Compose, net : String) -> String {
if x_flag(compose, "default_net_name_compat") {
join_name_parts(compose, [
compose.project_name.replace(old="-", new=""),
net,
])
} else {
format_name(compose, [net])
}
}
///|
/// The networks a container joins: the service's `networks`, or the project's
/// default network.
pub fn container_networks(
compose : Compose,
cnt : @yaml.Yaml,
) -> Map[String, @yaml.Yaml] {
match y_get(cnt, "networks") {
Some(n) =>
match n {
@yaml.Yaml::Map(m) => m
@yaml.Yaml::Array(_) => {
let out : Map[String, @yaml.Yaml] = Map([])
for item in y_list(n) {
match y_str(item) {
Some(s) => out[s] = y_obj([])
None => ()
}
}
out
}
_ => Map([])
}
None =>
match compose.default_net {
Some(net) => {
let out : Map[String, @yaml.Yaml] = Map([])
out[net] = y_obj([])
out
}
None => Map([])
}
}
}
///|
///|
/// Ensure every network and named volume the given containers need exists.
///
/// The probes are deduplicated by name: a project with N containers on one
/// network, or N services sharing one volume, pays one `podman network exists`
/// / `podman volume inspect` instead of one per container.
pub async fn assert_networks_and_volumes(
compose : Compose,
podman : Podman,
containers : Array[@yaml.Yaml],
) -> Unit raise Error {
let nets : Map[String, Unit] = Map([])
let vols : Map[String, Unit] = Map([])
for cnt in containers {
match y_get(cnt, "network_mode") {
Some(_) => ()
None =>
for entry in container_networks(compose, cnt) {
nets[entry.0] = ()
}
}
for mount in container_mounts(compose, cnt) {
if mount.kind != "volume" {
continue
}
match mount.source {
Some(key) => vols[key] = ()
None => ()
}
}
}
for net in nets {
ensure_network(compose, podman, net.0)
}
for vol in vols {
ensure_volume(compose, podman, vol.0)
}
}
///|
/// Whether a top-level volume declaration is `external` (in either the boolean
/// or the `{name: ...}` form).
fn volume_is_external(decl : @yaml.Yaml) -> Bool {
match y_get(decl, "external") {
Some(ext) =>
match ext {
@yaml.Yaml::Boolean(b) => b
@yaml.Yaml::Map(_) => true
_ => false
}
None => false
}
}
///|
/// Create one network unless it already exists.
async fn ensure_network(
compose : Compose,
podman : Podman,
net : String,
) -> Unit raise Error {
let name = network_name(compose, net)
match podman_try_output(podman, "network", ["exists", name]) {
Some(_) => ()
None => {
let args : Array[String] = ["create"]
match compose.networks.get(net) {
Some(desc) => {
match y_get(desc, "driver") {
Some(d) =>
match y_str(d) {
Some(s) if s.length() > 0 => {
args.push("--driver")
args.push(s)
}
_ => ()
}
None => ()
}
if y_bool(y_get(desc, "internal").unwrap_or(y_null())).unwrap_or(
false,
) {
args.push("--internal")
}
match y_get(desc, "driver_opts") {
Some(opts) =>
match y_map(opts) {
Some(om) =>
for e in om {
match y_str(e.1) {
Some(s) => {
args.push("--opt")
args.push(e.0 + "=" + s)
}
None => ()
}
}
None => ()
}
None => ()
}
for label in y_env_list(y_get(desc, "labels").unwrap_or(y_null())) {
args.push("--label")
args.push(label)
}
}
None => ()
}
args.push("--label")
args.push("io.podman.compose.project=" + compose.project_name)
args.push("--label")
args.push("com.docker.compose.project=" + compose.project_name)
args.push(name)
ignore(podman_run(podman, "network", args))
}
}
}
///|
/// Create one named volume unless it already exists.
async fn ensure_volume(
compose : Compose,
podman : Podman,
key : String,
) -> Unit raise Error {
let name = resolve_volume_name(compose, y_null(), key, "")
match podman_try_output(podman, "volume", ["inspect", name]) {
Some(_) => ()
None => {
let decl = compose.vols.get(key)
match decl {
Some(d) =>
if volume_is_external(d) {
raise fail(
"external volume [" +
name +
"] does not exist; create it first with: podman volume create '" +
name +
"'",
)
}
None => ()
}
let args : Array[String] = [
"create",
"--label",
"io.podman.compose.project=" + compose.project_name,
"--label",
"com.docker.compose.project=" + compose.project_name,
]
match decl {
Some(d) => {
match y_get(d, "driver") {
Some(dr) =>
match y_str(dr) {
Some(s) if s.length() > 0 => {
args.push("--driver")
args.push(s)
}
_ => ()
}
None => ()
}
match y_get(d, "driver_opts") {
Some(opts) =>
match y_map(opts) {
Some(om) =>
for e in om {
match y_str(e.1) {
Some(s) => {
args.push("--opt")
args.push(e.0 + "=" + s)
}
None => ()
}
}
None => ()
}
None => ()
}
for label in y_env_list(y_get(d, "labels").unwrap_or(y_null())) {
args.push("--label")
args.push(label)
}
}
None => ()
}
args.push(name)
ignore(podman_run(podman, "volume", args))
}
}
}