diff options
author | Matthias Beyer <mail@beyermatthias.de> | 2021-03-10 10:32:24 +0100 |
---|---|---|
committer | Matthias Beyer <mail@beyermatthias.de> | 2021-03-10 11:48:45 +0100 |
commit | 0ecac14b26d6eea26328aefff1c4e88d25cd643c (patch) | |
tree | ba35fcf8ccbdc36384794826dfb7b05918351580 /src/commands | |
parent | a554772d25026a9cf223514d9422c70fc9b15f69 (diff) | |
parent | 9a79643ced98567ab7b0c742f0d161cb5dd43578 (diff) |
Merge branch 'subcommand-endpoint'
Conflicts:
src/cli.rs
src/main.rs
from merging the "metrics" subcommand implementation branch first.
Conflicts were trivial, so I resolved them here in the merge commit.
Signed-off-by: Matthias Beyer <mail@beyermatthias.de>
Diffstat (limited to 'src/commands')
-rw-r--r-- | src/commands/db.rs | 87 | ||||
-rw-r--r-- | src/commands/endpoint.rs | 300 | ||||
-rw-r--r-- | src/commands/endpoint_container.rs | 465 | ||||
-rw-r--r-- | src/commands/mod.rs | 4 | ||||
-rw-r--r-- | src/commands/util.rs | 68 |
5 files changed, 850 insertions, 74 deletions
diff --git a/src/commands/db.rs b/src/commands/db.rs index 349fa79..0f11837 100644 --- a/src/commands/db.rs +++ b/src/commands/db.rs @@ -8,7 +8,6 @@ // SPDX-License-Identifier: EPL-2.0 // -use std::fmt::Display; use std::io::Write; use std::path::PathBuf; use std::process::Command; @@ -141,7 +140,7 @@ fn artifacts(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { use crate::schema::artifacts::dsl; let csv = matches.is_present("csv"); - let hdrs = mk_header(vec!["id", "path", "released", "job id"]); + let hdrs = crate::commands::util::mk_header(vec!["id", "path", "released", "job id"]); let conn = crate::db::establish_connection(conn_cfg)?; let data = matches .value_of("job_uuid") @@ -180,7 +179,7 @@ fn artifacts(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { if data.is_empty() { info!("No artifacts in database"); } else { - display_data(hdrs, data, csv)?; + crate::commands::util::display_data(hdrs, data, csv)?; } Ok(()) @@ -190,7 +189,7 @@ fn envvars(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { use crate::schema::envvars::dsl; let csv = matches.is_present("csv"); - let hdrs = mk_header(vec!["id", "name", "value"]); + let hdrs = crate::commands::util::mk_header(vec!["id", "name", "value"]); let conn = crate::db::establish_connection(conn_cfg)?; let data = dsl::envvars .load::<models::EnvVar>(&conn)? @@ -201,7 +200,7 @@ fn envvars(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { if data.is_empty() { info!("No environment variables in database"); } else { - display_data(hdrs, data, csv)?; + crate::commands::util::display_data(hdrs, data, csv)?; } Ok(()) @@ -211,7 +210,7 @@ fn images(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { use crate::schema::images::dsl; let csv = matches.is_present("csv"); - let hdrs = mk_header(vec!["id", "name"]); + let hdrs = crate::commands::util::mk_header(vec!["id", "name"]); let conn = crate::db::establish_connection(conn_cfg)?; let data = dsl::images .load::<models::Image>(&conn)? @@ -222,7 +221,7 @@ fn images(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { if data.is_empty() { info!("No images in database"); } else { - display_data(hdrs, data, csv)?; + crate::commands::util::display_data(hdrs, data, csv)?; } Ok(()) @@ -230,7 +229,7 @@ fn images(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { fn submits(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { let csv = matches.is_present("csv"); - let hdrs = mk_header(vec!["id", "time", "uuid"]); + let hdrs = crate::commands::util::mk_header(vec!["id", "time", "uuid"]); let conn = crate::db::establish_connection(conn_cfg)?; // Helper to map Submit -> Vec<String> @@ -284,7 +283,7 @@ fn submits(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { if data.is_empty() { info!("No submits in database"); } else { - display_data(hdrs, data, csv)?; + crate::commands::util::display_data(hdrs, data, csv)?; } Ok(()) @@ -294,7 +293,7 @@ fn jobs(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { use crate::schema::jobs::dsl; let csv = matches.is_present("csv"); - let hdrs = mk_header(vec![ + let hdrs = crate::commands::util::mk_header(vec![ "id", "submit uuid", "job uuid", @@ -410,7 +409,7 @@ fn jobs(conn_cfg: DbConnectionConfig, matches: &ArgMatches) -> Result<()> { if data.is_empty() { info!("No submits in database"); } else { - display_data(hdrs, data, csv)?; + crate::commands::util::display_data(hdrs, data, csv)?; } Ok(()) @@ -448,7 +447,7 @@ fn job(conn_cfg: DbConnectionConfig, config: &Configuration, matches: &ArgMatche let success = parsed_log.is_successfull(); if csv { - let hdrs = mk_header(vec![ + let hdrs = crate::commands::util::mk_header(vec![ "UUID", "success", "Package Name", @@ -471,7 +470,7 @@ fn job(conn_cfg: DbConnectionConfig, config: &Configuration, matches: &ArgMatche data.4.name.to_string(), data.0.container_hash, ]]; - display_data(hdrs, data, csv) + crate::commands::util::display_data(hdrs, data, csv) } else { let env_vars = if matches.is_present("show_env") { Some({ @@ -595,7 +594,7 @@ fn job(conn_cfg: DbConnectionConfig, config: &Configuration, matches: &ArgMatche fn releases(conn_cfg: DbConnectionConfig, config: &Configuration, matches: &ArgMatches) -> Result<()> { let csv = matches.is_present("csv"); let conn = crate::db::establish_connection(conn_cfg)?; - let header = mk_header(["Package", "Version", "Date", "Path"].to_vec()); + let header = crate::commands::util::mk_header(["Package", "Version", "Date", "Path"].to_vec()); let data = schema::jobs::table .inner_join(schema::packages::table) .inner_join(schema::artifacts::table) @@ -632,64 +631,6 @@ fn releases(conn_cfg: DbConnectionConfig, config: &Configuration, matches: &ArgM }) .collect::<Vec<Vec<_>>>(); - display_data(header, data, csv) -} - - -fn mk_header(vec: Vec<&str>) -> Vec<ascii_table::Column> { - vec.into_iter() - .map(|name| ascii_table::Column { - header: name.into(), - align: ascii_table::Align::Left, - ..Default::default() - }) - .collect() -} - -/// Display the passed data as nice ascii table, -/// or, if stdout is a pipe, print it nicely parseable -fn display_data<D: Display>( - headers: Vec<ascii_table::Column>, - data: Vec<Vec<D>>, - csv: bool, -) -> Result<()> { - if csv { - use csv::WriterBuilder; - let mut wtr = WriterBuilder::new().from_writer(vec![]); - for record in data.into_iter() { - let r: Vec<String> = record.into_iter().map(|e| e.to_string()).collect(); - - wtr.write_record(&r)?; - } - - let out = std::io::stdout(); - let mut lock = out.lock(); - - wtr.into_inner() - .map_err(Error::from) - .and_then(|t| String::from_utf8(t).map_err(Error::from)) - .and_then(|text| writeln!(lock, "{}", text).map_err(Error::from)) - } else if atty::is(atty::Stream::Stdout) { - let mut ascii_table = ascii_table::AsciiTable { - columns: Default::default(), - max_width: terminal_size::terminal_size() - .map(|tpl| tpl.0 .0 as usize) // an ugly interface indeed! - .unwrap_or(80), - }; - - headers.into_iter().enumerate().for_each(|(i, c)| { - ascii_table.columns.insert(i, c); - }); - - ascii_table.print(data); - Ok(()) - } else { - let out = std::io::stdout(); - let mut lock = out.lock(); - for list in data { - writeln!(lock, "{}", list.iter().map(|d| d.to_string()).join(" "))?; - } - Ok(()) - } + crate::commands::util::display_data(header, data, csv) } diff --git a/src/commands/endpoint.rs b/src/commands/endpoint.rs new file mode 100644 index 0000000..c1632e6 --- /dev/null +++ b/src/commands/endpoint.rs @@ -0,0 +1,300 @@ +// +// Copyright (c) 2020-2021 science+computing ag and other contributors +// +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// + +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::Error; +use anyhow::Result; +use anyhow::anyhow; +use clap::ArgMatches; +use log::{debug, info}; +use itertools::Itertools; +use tokio_stream::StreamExt; + +use crate::config::Configuration; +use crate::util::progress::ProgressBars; +use crate::endpoint::Endpoint; + +pub async fn endpoint(matches: &ArgMatches, config: &Configuration, progress_generator: ProgressBars) -> Result<()> { + let endpoint_names = matches + .value_of("endpoint_name") + .map(String::from) + .map(|ep| vec![ep]) + .unwrap_or_else(|| { + config.docker() + .endpoints() + .iter() + .map(|ep| ep.name()) + .cloned() + .collect() + }); + + match matches.subcommand() { + Some(("ping", matches)) => ping(endpoint_names, matches, config, progress_generator).await, + Some(("stats", matches)) => stats(endpoint_names, matches, config, progress_generator).await, + Some(("container", matches)) => crate::commands::endpoint_container::container(endpoint_names, matches, config).await, + Some(("containers", matches)) => containers(endpoint_names, matches, config).await, + Some((other, _)) => Err(anyhow!("Unknown subcommand: {}", other)), + None => Err(anyhow!("No subcommand")), + } +} + +async fn ping(endpoint_names: Vec<String>, + matches: &ArgMatches, + config: &Configuration, + progress_generator: ProgressBars +) -> Result<()> { + let n_pings = matches.value_of("ping_n").map(u64::from_str).transpose()?.unwrap(); // safe by clap + let sleep = matches.value_of("ping_sleep").map(u64::from_str).transpose()?.unwrap(); // safe by clap + let endpoints = connect_to_endpoints(config, &endpoint_names).await?; + let multibar = Arc::new({ + let mp = indicatif::MultiProgress::new(); + if progress_generator.hide() { + mp.set_draw_target(indicatif::ProgressDrawTarget::hidden()); + } + mp + }); + + let ping_process = endpoints + .iter() + .map(|endpoint| { + let bar = multibar.add(progress_generator.bar()); + bar.set_length(n_pings); + bar.set_message(&format!("Pinging {}", endpoint.name())); + + async move { + for i in 1..(n_pings + 1) { + debug!("Pinging {} for the {} time", endpoint.name(), i); + let r = endpoint.ping().await; + bar.inc(1); + if let Err(e) = r { + bar.finish_with_message(&format!("Pinging {} failed", endpoint.name())); + return Err(e) + } + + tokio::time::sleep(tokio::time::Duration::from_secs(sleep)).await; + } + + bar.finish_with_message(&format!("Pinging {} successful", endpoint.name())); + Ok(()) + } + }) + .collect::<futures::stream::FuturesUnordered<_>>() + .collect::<Result<()>>(); + + let multibar_block = tokio::task::spawn_blocking(move || multibar.join()); + tokio::join!(ping_process, multibar_block).0 +} + +async fn stats(endpoint_names: Vec<String>, + matches: &ArgMatches, + config: &Configuration, + progress_generator: ProgressBars +) -> Result<()> { + let csv = matches.is_present("csv"); + let endpoints = connect_to_endpoints(config, &endpoint_names).await?; + let bar = progress_generator.bar(); + bar.set_length(endpoint_names.len() as u64); + bar.set_message("Fetching stats"); + + let hdr = crate::commands::util::mk_header([ + "Name", + "Containers", + "Images", + "Kernel", + "Memory", + "Memory limit", + "Cores", + "OS", + "System Time", + ].to_vec()); + + let data = endpoints + .into_iter() + .map(|endpoint| { + let bar = bar.clone(); + async move { + let r = endpoint.stats().await; + bar.inc(1); + r + } + }) + .collect::<futures::stream::FuturesUnordered<_>>() + .collect::<Result<Vec<_>>>() + .await + .map_err(|e| { + bar.finish_with_message("Fetching stats errored"); + e + })? + .into_iter() + .map(|stat| { + vec![ + stat.name, + stat.containers.to_string(), + stat.images.to_string(), + stat.kernel_version, + bytesize::ByteSize::b(stat.mem_total).to_string(), + stat.memory_limit.to_string(), + stat.n_cpu.to_string(), + stat.operating_system.to_string(), + stat.system_time.unwrap_or_else(|| String::from("unknown")), + ] + }) + .collect(); + + bar.finish_with_message("Fetching stats successful"); + crate::commands::util::display_data(hdr, data, csv) +} + + +async fn containers(endpoint_names: Vec<String>, + matches: &ArgMatches, + config: &Configuration, +) -> Result<()> { + match matches.subcommand() { + Some(("list", matches)) => containers_list(endpoint_names, matches, config).await, + Some(("prune", matches)) => containers_prune(endpoint_names, matches, config).await, + Some((other, _)) => Err(anyhow!("Unknown subcommand: {}", other)), + None => Err(anyhow!("No subcommand")), + } +} + +async fn containers_list(endpoint_names: Vec<String>, + matches: &ArgMatches, + config: &Configuration, +) -> Result<()> { + let list_stopped = matches.is_present("list_stopped"); + let filter_image = matches.value_of("filter_image"); + let older_than_filter = get_date_filter("older_than", matches)?; + let newer_than_filter = get_date_filter("newer_than", matches)?; + let csv = matches.is_present("csv"); + let hdr = crate::commands::util::mk_header([ + "Endpoint", + "Container id", + "Image", + "Created", + "Status", + ].to_vec()); + + let data = connect_to_endpoints(config, &endpoint_names) + .await? + .into_iter() + .map(|ep| async move { + ep.container_stats().await.map(|stats| (ep.name().clone(), stats)) + }) + .collect::<futures::stream::FuturesUnordered<_>>() + .collect::<Result<Vec<(_, _)>>>() + .await? + .into_iter() + .map(|tpl| { + let endpoint_name = tpl.0; + tpl.1 + .into_iter() + .filter(|stat| list_stopped || stat.state != "exited") + .filter(|stat| filter_image.map(|fim| fim == stat.image).unwrap_or(true)) + .filter(|stat| older_than_filter.as_ref().map(|time| time > &stat.created).unwrap_or(true)) + .filter(|stat| newer_than_filter.as_ref().map(|time| time < &stat.created).unwrap_or(true)) + .map(|stat| { + vec![ + endpoint_name.clone(), + stat.id, + stat.image, + stat.created.to_string(), + stat.status, + ] + }) + .collect::<Vec<Vec<String>>>() + }) + .flatten() + .collect::<Vec<Vec<String>>>(); + + crate::commands::util::display_data(hdr, data, csv) +} + +async fn containers_prune(endpoint_names: Vec<String>, + matches: &ArgMatches, + config: &Configuration, +) -> Result<()> { + let older_than_filter = get_date_filter("older_than", matches)?; + let newer_than_filter = get_date_filter("newer_than", matches)?; + + let stats = connect_to_endpoints(config, &endpoint_names) + .await? + .into_iter() + .map(move |ep| async move { + let stats = ep.container_stats() + .await? + .into_iter() + .filter(|stat| stat.state == "exited") + .filter(|stat| older_than_filter.as_ref().map(|time| time > &stat.created).unwrap_or(true)) + .filter(|stat| newer_than_filter.as_ref().map(|time| time < &stat.created).unwrap_or(true)) + .map(|stat| (ep.clone(), stat)) + .collect::<Vec<(_, _)>>(); + Ok(stats) + }) + .collect::<futures::stream::FuturesUnordered<_>>() + .collect::<Result<Vec<_>>>() + .await?; + + let prompt = format!("Really delete {} Containers?", stats.iter().flatten().count()); + dialoguer::Confirm::new().with_prompt(prompt).interact()?; + + stats.into_iter() + .map(Vec::into_iter) + .flatten() + .map(|(ep, stat)| async move { + ep.get_container_by_id(&stat.id) + .await? + .ok_or_else(|| anyhow!("Failed to find existing container {}", stat.id))? + .delete() + .await + .map_err(Error::from) + }) + .collect::<futures::stream::FuturesUnordered<_>>() + .collect::<Result<()>>() + .await +} + +fn get_date_filter(name: &str, matches: &ArgMatches) -> Result<Option<chrono::DateTime::<chrono::Local>>> { + matches.value_of(name) + .map(humantime::parse_rfc3339_weak) + .transpose()? + .map(chrono::DateTime::<chrono::Local>::from) + .map(Ok) + .transpose() +} + +/// Helper function to connect to all endpoints from the configuration, that appear (by name) in +/// the `endpoint_names` list +pub(super) async fn connect_to_endpoints(config: &Configuration, endpoint_names: &[String]) -> Result<Vec<Arc<Endpoint>>> { + let endpoint_configurations = config + .docker() + .endpoints() + .iter() + .filter(|ep| endpoint_names.contains(ep.name())) + .cloned() + .map(|ep_cfg| { + crate::endpoint::EndpointConfiguration::builder() + .endpoint(ep_cfg) + .required_images(config.docker().images().clone()) + .required_docker_versions(config.docker().docker_versions().clone()) + .required_docker_api_versions(config.docker().docker_api_versions().clone()) + .build() + }) + .collect::<Vec<_>>(); + + info!("Endpoint config build"); + info!("Connecting to {n} endpoints: {eps}", + n = endpoint_configurations.len(), + eps = endpoint_configurations.iter().map(|epc| epc.endpoint().name()).join(", ")); + + crate::endpoint::util::setup_endpoints(endpoint_configurations).await +} diff --git a/src/commands/endpoint_container.rs b/src/commands/endpoint_container.rs new file mode 100644 index 0000000..1861cc7 --- /dev/null +++ b/src/commands/endpoint_container.rs @@ -0,0 +1,465 @@ +// +// Copyright (c) 2020-2021 science+computing ag and other contributors +// +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// + +use std::str::FromStr; + +use anyhow::Error; +use anyhow::Result; +use anyhow::anyhow; +use clap::ArgMatches; +use tokio_stream::StreamExt; +use shiplift::Container; + +use crate::config::Configuration; + +pub async fn container(endpoint_names: Vec<String>, + matches: &ArgMatches, + config: &Configuration, +) -> Result<()> { + let container_id = matches.value_of("container_id").unwrap(); + let endpoints = crate::commands::endpoint::connect_to_endpoints(config, &endpoint_names).await?; + let relevant_endpoints = endpoints.into_iter() + .map(|ep| async { + ep.has_container_with_id(container_id) + .await + .map(|b| (ep, b)) + }) + .collect::<futures::stream::FuturesUnordered<_>>() + .collect::<Result<Vec<(_, bool)>>>() + .await? + .into_iter() + .filter_map(|tpl| { + if tpl.1 { + Some(tpl.0) + } else { + None + } + }) + .collect::<Vec<_>>(); + + if relevant_endpoints.len() > 1 { + return Err(anyhow!("Found more than one container for id {}", container_id)) + } + + let relevant_endpoint = relevant_endpoints.get(0).ok_or_else(|| { + anyhow!("Found no container for id {}", container_id) + })?; + + let container = relevant_endpoint.get_container_by_id(container_id) + .await? + .ok_or_else(|| anyhow!("Cannot find container {} on {}", container_id, relevant_endpoint.name()))?; + + let confirm = |prompt: String| dialoguer::Confirm::new().with_prompt(prompt).interact(); + + match matches.subcommand() { + Some(("top", matches)) => top(matches, container).await, + Some(("kill", matches)) => { + confirm({ + if let Some(sig) = matches.value_of("signal").as_ref() { + format!("Really kill {} with {}?", container_id, sig) + } else { + format!("Really kill {}?", container_id) + } + })?; + + kill(matches, container).await + }, + Some(("delete", _)) => { + confirm(format!("Really delete {}?", container_id))?; + delete(container).await + }, + Some(("start", _)) => { + confirm(format!("Really start {}?", container_id))?; + start(container).await + }, + Some(("stop", matches)) => { + confirm(format!("Really stop {}?", container_id))?; + stop(matches, container).await + }, + Some(("exec", matches)) => { + confirm({ + let commands = matches.values_of("commands").unwrap().collect::<Vec<&str>>(); + format!("Really run '{}' in {}?", commands.join(" "), container_id) + })?; + exec(matches, container).await + }, + Some(("inspect", _)) => inspect(container).await, + Some((other, _)) => Err(anyhow!("Unknown subcommand: {}", other)), + None => Err(anyhow!("No subcommand")), + } +} + +async fn top(matches: &ArgMatches, container: Container<'_>) -> Result<()> { + let top = container.top(None).await?; + let hdr = crate::commands::util::mk_header(top.titles.iter().map(|s| s.as_ref()).collect()); + crate::commands::util::display_data(hdr, top.processes, matches.is_present("csv")) +} + +async fn kill(matches: &ArgMatches, container: Container<'_>) -> Result<()> { + let signal = matches.value_of("signal"); + container.kill(signal).await.map_err(Error::from) +} + +async fn delete(container: Container<'_>) -> Result<()> { + container.delete().await.map_err(Error::from) +} + +async fn start(container: Container<'_>) -> Result<()> { + container.start().await.map_err(Error::from) +} + +async fn stop(matches: &ArgMatches, container: Container<'_>) -> Result<()> { + container.stop({ + matches + .value_of("timeout") + .map(u64::from_str) + .transpose()? + .map(std::time::Duration::from_secs) + }) + .await + .map_err(Error::from) +} + +async fn exec(matches: &ArgMatches, container: Container<'_>) -> Result<()> { + use std::io::Write; + use futures::TryStreamExt; + + let execopts = shiplift::builder::ExecContainerOptions::builder() + .cmd({ + matches.values_of("commands").unwrap().collect::<Vec<&str>>() + }) + .attach_stdout(true) + .attach_stderr(true) + .build(); + + container.exec(&execopts) + .map_err(Error::from) + .try_for_each(|chunk| async { + match chunk { + shiplift::tty::TtyChunk::StdIn(_) => Err(anyhow!("Cannot handle STDIN TTY chunk")), + shiplift::tty::TtyChunk::StdOut(v) => { + std::io::stdout().write(&v).map_err(Error::from).map(|_| ()) + }, + shiplift::tty::TtyChunk::StdErr(v) => { + std::io::stderr().write(&v).map_err(Error::from).map(|_| ()) + }, + } + }) + .await +} + +// Print inspect details about the container +// +// +// ABANDON HOPE ALL YE WHO ENTER HERE +// +// (Dante) +// +// This is the most ugly function of the whole codebase. As ugly as it is: It is simply printing +// things, nothing here is too complex code-wise (except some nested formatting stuff...) +async fn inspect(container: Container<'_>) -> Result<()> { + use std::io::Write; + use itertools::Itertools; + + let d = container.inspect().await?; + + fn option_vec(ov: Option<&Vec<String>>) -> String { + ov.map(|v| format!("Some({})", v.iter().join(", "))).unwrap_or_else(|| String::from("None")) + } + + fn option_vec_nl(ov: Option<&Vec<String>>, ind: usize) -> String { + ov.map(|v| v.iter().map(|s| format!("{:ind$}{s}", "", ind = ind, s = s)).join("\n")).map(|s| format!("\n{}", s)).unwrap_or_else(|| String::from("None")) + } + + fn option_tostr<T: ToString>(ots: Option<T>) -> String { + ots.map(|s| format!("Some({})", s.to_string())).unwrap_or_else(|| String::from("None")) + } + + writeln!(std::io::stdout(), "{}", indoc::formatdoc!(r#" + Container: {container_id} + + app_armor_profile: {app_armor_profile} + args: {args} + config: + attach_stderr: {config_attach_stderr} + attach_stdin: {config_attach_stdin} + attach_stdout: {config_attach_stdout} + cmd: {config_cmd} + domainname: {config_domainname} + entrypoint: {config_entrypoint} + env: {config_env} + exposed_ports: {config_exposed_ports} + hostname: {config_hostname} + image: {config_image} + labels: {config_labels} + on_build: {config_on_build} + open_stdin: {config_open_stdin} + stdin_once: {config_stdin_once} + tty: {config_tty} + user: {config_user} + working_dir: {config_working_dir} + + created: {created} + driver: {driver} + host_config: + cgroup_parent: {host_config_cgroup_parent} + container_id_file: {host_config_container_id_file} + cpu_shares: {host_config_cpu_shares} + cpuset_cpus: {host_config_cpuset_cpus} + memory: {host_config_memory} + memory_swap: {host_config_memory_swap} + network_mode: {host_config_network_mode} + pid_mode: {host_config_pid_mode} + port_bindings: {host_config_port_bindings} + privileged: {host_config_privileged} + publish_all_ports: {host_config_publish_all_ports} + readonly_rootfs: {host_config_readonly_rootfs} + hostname_path: {hostname_path} + hosts_path: {hosts_path} + log_path: {log_path} + id: {id} + image: {image} + mount_label: {mount_label} + name: {name} + network_settings: + bridge: {network_settings_bridge} + gateway: {network_settings_gateway} + ip_address: {network_settings_ip_address} + ip_prefix_len: {network_settings_ip_prefix_len} + mac_address: {network_settings_mac_address} + ports: {network_settings_ports} + networks: {network_settings_networks} + path: {path} + process_label: {process_label} + resolv_conf_path: {resolv_conf_path} + restart_count: {restart_count} + state: + error: {state_error} + exit_code: {state_exit_code} + finished_at: {state_finished_at} + oom_killed: {state_oom_killed} + paused: {state_paused} + pid: {state_pid} + restarting: {state_restarting} + running: {state_running} + started_at: {state_started_at} + status: {state_status} + mounts: {mounts} + "#, + + container_id = container.id(), + + app_armor_profile = d.app_armor_profile, + args = d.args.iter().join(", "), + + config_attach_stderr = d.config.attach_stderr.to_string(), + config_attach_stdin = d.config.attach_stdin.to_string(), + config_attach_stdout = d.config.attach_stdout.to_string(), + config_cmd = option_vec(d.config.cmd.as_ref()), + config_domainname = d.config.domainname, + config_entrypoint = option_vec(d.config.entrypoint.as_ref()), + config_env = option_vec_nl(d.config.env.as_ref(), 8), + config_exposed_ports = { + d.config.exposed_ports.map(|hm| { + let s = hm.iter() + .map(|(k, v_hm)| { + format!("{:ind$}{k}:\n{hm}", + "", ind = 8, + k = k, + hm = v_hm.iter() + .map(|(k, v)| format!("{:ind$}{k}: {v}", "", ind = 12, k = k, v = v)) + .collect::<Vec<_>>() + .join("\n") + ) + }) + .collect::<Vec<_>>() + .join("\n"); + + format!("\n{s}", s = s) + }) + .unwrap_or_else(|| String::from("None")) + }, + config_hostname = d.config.hostname, + config_image = d.config.image, + config_labels = { + d.config.labels + .map(|hm| { + let s = hm.iter() + .map(|(k, v)| format!("{:ind$}{k}: {v}", "", ind = 8, k = k, v = v)) + .collect::<Vec<_>>() + .join("\n"); + format!("\n{s}", s = s) + }) + .unwrap_or_else(|| String::from("None")) + }, + config_on_build = option_vec(d.config.on_build.as_ref()), + config_open_stdin = d.config.open_stdin.to_string(), + config_stdin_once = d.config.stdin_once.to_string(), + config_tty = d.config.tty.to_string(), + config_user = d.config.user, + config_working_dir = d.config.working_dir, + + created = d.created.to_string(), + driver = d.driver, + + host_config_cgroup_parent = option_tostr(d.host_config.cgroup_parent.as_ref()), + host_config_container_id_file = d.host_config.container_id_file, + host_config_cpu_shares = option_tostr(d.host_config.cpu_shares.as_ref()), + host_config_cpuset_cpus = option_tostr(d.host_config.cpuset_cpus.as_ref()), + host_config_memory = option_tostr(d.host_config.memory.as_ref()), + host_config_memory_swap = option_tostr(d.host_config.memory_swap.as_ref()), + host_config_network_mode = d.host_config.network_mode, + host_config_pid_mode = option_tostr(d.host_config.pid_mode.as_ref()), + host_config_port_bindings = { + d.host_config.port_bindings + .map(|hm| { + let s = hm.iter() + .map(|(k, v)| { + let v = v.iter() + .map(|hm| { + hm.iter() + .map(|(k, v)| { + format!("{:ind$}{k}: {v}", "", ind = 12, k = k, v = v) + }) + .collect::<Vec<_>>() + .join("\n") + }) + .collect::<Vec<_>>() + .join("\n"); + + format!("{:ind$}{k}: {v}", "", ind = 8, k = k, v = format!("\n{}", v)) + }) + .collect::<Vec<_>>() + .join("\n"); + format!("\n{s}", s = s) + }) + .unwrap_or_else(|| String::from("None")) + }, + host_config_privileged = d.host_config.privileged.to_string(), + host_config_publish_all_ports = d.host_config.publish_all_ports.to_string(), + host_config_readonly_rootfs = option_tostr(d.host_config.readonly_rootfs.as_ref()), + + hostname_path = d.hostname_path, + hosts_path = d.hosts_path, + log_path = d.log_path, + id = d.id, + image = d.image, + mount_label = d.mount_label, + name = d.name, + + network_settings_bridge = d.network_settings.bridge, + network_settings_gateway = d.network_settings.gateway, + network_settings_ip_address = d.network_settings.ip_address, + network_settings_ip_prefix_len = d.network_settings.ip_prefix_len.to_string(), + network_settings_mac_address = d.network_settings.mac_address, + network_settings_ports = { + d.network_settings.ports + .map(|hm| { + let s = hm.iter() + .map(|(k, v)| { + let v = v.as_ref().map(|v| { + v.iter() + .map(|hm| { + let s = hm.iter() + .map(|(k, v)| format!("{:ind$}{k}: {v}", "", ind = 12, k = k, v = v)) + .collect::<Vec<_>>() + .join("\n"); + format!("\n{s}", s = s) + }) + .collect::<Vec<_>>() + .join("\n") + }).unwrap_or_else(|| String::from("None")); + + format!("{:ind$}{k}: {v}", "", ind = 8, k = k, v = format!("\n{}", v)) + }) + .collect::<Vec<_>>() + .join("\n"); + format!("\n{s}", s = s) + }) + .unwrap_or_else(|| String::from("None")) + }, + network_settings_networks = { + let s = d.network_settings.networks.iter().map(|(k, v)| { + indoc::formatdoc!(r#" + {k}: + network_id: {network_id} + endpoint_id: {endpoint_id} + gateway: {gateway} + ip_address: {ip_address} + ip_prefix_len: {ip_prefix_len} + ipv6_gateway: {ipv6_gateway} + global_ipv6_address: {global_ipv6_address} + global_ipv6_prefix_len: {global_ipv6_prefix_len} + mac_address: {mac_address} + "#, + k = k, + network_id = v.network_id, + endpoint_id = v.endpoint_id, + gateway = v.gateway, + ip_address = v.ip_address, + ip_prefix_len = v.ip_prefix_len, + ipv6_gateway = v.ipv6_gateway, + global_ipv6_address = v.global_ipv6_address, + global_ipv6_prefix_len = v.global_ipv6_prefix_len.to_string(), + mac_address = v.mac_address, + ) + .lines() + .map(|s| format!("{:ind$}{s}", "", ind = 8, s = s)) + .join("\n") + }) + .collec |