use std::fmt::{Display, Formatter};
use std::io;
use std::io::Write;
use std::path::Path;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use color_eyre::eyre::{OptionExt, bail};
use tracing::{debug, error, warn};
use wildmatch::WildMatch;
use crate::command::CommandExt;
use crate::error::{SkipStep, TopgradeError};
use crate::terminal::print_separator;
use crate::{execution_context::ExecutionContext, utils::require};
use rust_i18n::t;
const NONEXISTENT_REPO: &str = "repository does not exist";
const DOCKER_NOT_RUNNING: &str = "We recommend to activate the WSL integration in Docker Desktop settings.";
#[derive(Debug)]
struct Container {
repo_tag: String,
platform: String,
}
impl Container {
fn new(repo_tag: String, platform: String) -> Self {
Self { repo_tag, platform }
}
}
impl Display for Container {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
t!(
"`{repo_tag}` for `{platform}`",
repo_tag = self.repo_tag,
platform = self.platform
)
)
}
}
fn list_containers(ctx: &ExecutionContext, crt: &Path) -> Result<Vec<Container>> {
let ignored_containers = ctx.config().containers_ignored_tags().map(|patterns| {
patterns
.iter()
.map(|pattern| WildMatch::new(pattern))
.collect::<Vec<WildMatch>>()
});
debug!(
"Querying '{} image ls --format \"{{{{.Repository}}}}:{{{{.Tag}}}}/{{{{.ID}}}}\"' for containers",
crt.display()
);
let sudo = if ctx.config().containers_use_sudo() {
Some(ctx.require_sudo()?)
} else {
None
};
let output = if let Some(sudo) = sudo {
sudo.execute(ctx, crt)?
.always()
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
.output_checked_utf8()?
} else {
ctx.execute(crt)
.always()
.args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
.output_checked_utf8()?
};
let mut retval = vec![];
for line in output.stdout.lines() {
if line.starts_with("localhost") {
debug!("Skipping self-built container '{}'", line);
continue;
}
if line.contains("<none>") {
debug!("Skipping bogus container '{}'", line);
continue;
}
if line.starts_with("vsc-") {
debug!("Skipping visual studio code dev container '{}'", line);
continue;
}
debug!("Using container '{}'", line);
let split_res = line.split(' ').collect::<Vec<&str>>();
if split_res.len() != 2 {
bail!(format!(
"Got erroneous output from `{} image ls --format \"{{.Repository}}:{{.Tag}} {{.ID}}\"; Expected line to split into 2 parts",
crt.display()
));
}
let (repo_tag, image_id) = (split_res[0], split_res[1]);
if let Some(ref ignored_containers) = ignored_containers
&& ignored_containers.iter().any(|pattern| pattern.matches(repo_tag))
{
debug!("Skipping ignored container '{}'", line);
continue;
}
debug!(
"Querying '{} image inspect --format \"{{{{.Os}}}}/{{{{.Architecture}}}}\"' for container {}",
crt.display(),
image_id
);
let inspect_output = if let Some(sudo) = sudo {
sudo.execute(ctx, crt)?
.always()
.args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"])
.output_checked_utf8()?
} else {
ctx.execute(crt)
.always()
.args(["image", "inspect", image_id, "--format", "{{.Os}}/{{.Architecture}}"])
.output_checked_utf8()?
};
let mut platform = inspect_output.stdout;
platform.truncate(platform.len() - 1);
if !platform.contains('/') {
bail!(format!(
"Got erroneous output from `{} image ls --format \"{{.Repository}}:{{.Tag}} {{.ID}}\"; Expected platform to contain '/'",
crt.display()
));
}
retval.push(Container::new(repo_tag.to_string(), platform));
}
Ok(retval)
}
pub fn run_containers(ctx: &ExecutionContext) -> Result<()> {
let container_runtime = ctx.config().containers_runtime().to_string();
let use_sudo = ctx.config().containers_use_sudo();
let crt = require(container_runtime)?;
debug!("Using container runtime '{}'", crt.display());
print_separator(t!("Containers"));
let sudo = if use_sudo { Some(ctx.require_sudo()?) } else { None };
let output = if let Some(sudo) = sudo {
sudo.execute(ctx, &crt)?
.always()
.arg("--help")
.output_checked_with(|_| Ok(()))?
} else {
ctx.execute(&crt)
.always()
.arg("--help")
.output_checked_with(|_| Ok(()))?
};
let status_code = output
.status
.code()
.ok_or_eyre("Couldn't get status code (terminated by signal)")?;
let stdout = std::str::from_utf8(&output.stdout).wrap_err("Expected output to be valid UTF-8")?;
if stdout.contains(DOCKER_NOT_RUNNING) && status_code == 1 {
io::stdout().write_all(&output.stdout)?;
io::stderr().write_all(&output.stderr)?;
warn!(
"{} seems to be non-functional right now (see above). Is WSL integration enabled for Docker Desktop? Is Docker Desktop running?",
crt.display()
);
return Err(SkipStep(format!(
"{} seems to be non-functional right now. Possibly WSL integration is not enabled for Docker Desktop, or Docker Desktop is not running.",
crt.display()
)).into());
} else if !output.status.success() {
io::stdout().write_all(&output.stdout)?;
io::stderr().write_all(&output.stderr)?;
bail!(
"{0} seems to be non-functional (`{0} --help` returned non-zero exit code {1})",
crt.display(),
status_code,
);
}
let containers = list_containers(ctx, &crt).context("Failed to list Docker containers")?;
debug!("Containers to inspect: {:?}", containers);
for container in &containers {
debug!("Pulling container '{}'", container);
let mut args = vec!["pull", container.repo_tag.as_str()];
if container.platform.as_str() != "/" {
args.push("--platform");
args.push(container.platform.as_str());
}
let mut exec = if let Some(sudo) = sudo {
sudo.execute(ctx, &crt)?
} else {
ctx.execute(&crt)
};
if let Err(e) = exec.args(&args).status_checked() {
error!("Pulling container '{}' failed: {}", container, e);
if match exec.output_checked_utf8() {
Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO),
Err(e) => match e.downcast_ref::<TopgradeError>() {
Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO),
_ => false,
},
} {
warn!("Skipping unknown container '{}'", container);
continue;
}
return Err(e);
}
}
if ctx.config().containers_system_prune() {
if let Some(sudo) = sudo {
sudo.execute(ctx, &crt)?
.args(["system", "prune", "--force"])
.status_checked()?
} else {
ctx.execute(&crt)
.args(["system", "prune", "--force"])
.status_checked()?
}
} else if ctx.config().cleanup() {
debug!("Removing dangling images");
if let Some(sudo) = sudo {
sudo.execute(ctx, &crt)?
.args(["image", "prune", "-f"])
.status_checked()?
} else {
ctx.execute(&crt).args(["image", "prune", "-f"]).status_checked()?
}
}
Ok(())
}