summaryrefslogtreecommitdiffstats
path: root/src/commands
diff options
context:
space:
mode:
authorMatthias Beyer <mail@beyermatthias.de>2021-03-10 10:32:24 +0100
committerMatthias Beyer <mail@beyermatthias.de>2021-03-10 11:48:45 +0100
commit0ecac14b26d6eea26328aefff1c4e88d25cd643c (patch)
treeba35fcf8ccbdc36384794826dfb7b05918351580 /src/commands
parenta554772d25026a9cf223514d9422c70fc9b15f69 (diff)
parent9a79643ced98567ab7b0c742f0d161cb5dd43578 (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.rs87
-rw-r--r--src/commands/endpoint.rs300
-rw-r--r--src/commands/endpoint_container.rs465
-rw-r--r--src/commands/mod.rs4
-rw-r--r--src/commands/util.rs68
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