From 72ee6598a6ecd82bb75d03ffb04689af883f3e5b Mon Sep 17 00:00:00 2001 From: Funky185540 Date: Sat, 5 Mar 2022 20:59:19 +0100 Subject: [PATCH] Upgrade podman/docker containers (#850) * containers: Pull newer versions of containers Allows topgrade to update a users containers. It will automatically skip containers which come from the `localhost` repo as these are self-built. Respects the version number the containers were initially checked out with in order not to introduce semver-breaking changes. Works with podman and docker. * topgrade: Add 'containers' step * containers: Ignore some errors for docker This patch is needed to achieve compatibility between docker and podman. In particular, docker doesn't store/tell the user from which repository (i.e. `hub.docker.com`, or `registry.fedoraproject.org`) a container originates. This has the side-effect, that self-built containers cannot be distinguished from publicly available containers. Therefore this patch introduces an exception to the error handling when pulling, by scanning the output of the `docker pull` command. If it finds the `registry does not exist` substring in the output, it will skip the container but **NOT** consider the whole update step failed. * containers: Skip '' containers that result from either intermediate products of a container build or when images are dangling. * steps: containers: simplify error handling And don't return errors from within the "unknown container registry" handling, since that would immediately terminate the whole update which isn't intended. --- src/config.rs | 1 + src/main.rs | 1 + src/steps/containers.rs | 104 ++++++++++++++++++++++++++++++++++++++++ src/steps/mod.rs | 1 + 4 files changed, 107 insertions(+) create mode 100644 src/steps/containers.rs diff --git a/src/config.rs b/src/config.rs index d79b0ce3..712d9918 100644 --- a/src/config.rs +++ b/src/config.rs @@ -78,6 +78,7 @@ pub enum Step { Composer, Conda, ConfigUpdate, + Containers, CustomCommands, Deno, Dotnet, diff --git a/src/main.rs b/src/main.rs index 27413af8..b89199e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -312,6 +312,7 @@ fn run() -> Result<()> { runner.execute(Step::Kakoune, "Kakoune", || kakoune::upgrade_kak_plug(&ctx))?; runner.execute(Step::Node, "npm", || node::run_npm_upgrade(&ctx))?; runner.execute(Step::Pnpm, "pnpm", || node::pnpm_global_update(&ctx))?; + runner.execute(Step::Containers, "Containers", || containers::run_containers(&ctx))?; runner.execute(Step::Deno, "deno", || node::deno_upgrade(&ctx))?; runner.execute(Step::Composer, "composer", || generic::run_composer_update(&ctx))?; runner.execute(Step::Krew, "krew", || generic::run_krew_upgrade(run_type))?; diff --git a/src/steps/containers.rs b/src/steps/containers.rs new file mode 100644 index 00000000..4a34d27a --- /dev/null +++ b/src/steps/containers.rs @@ -0,0 +1,104 @@ +use anyhow::Result; + +use crate::error::{self, TopgradeError}; +use crate::executor::CommandExt; +use crate::terminal::print_separator; +use crate::{execution_context::ExecutionContext, utils::require}; +use log::{debug, error, warn}; +use std::path::Path; +use std::process::Command; + +// A string found in the output of docker for containers that weren't found in +// the docker registry. We use this to gracefully handle and skip containers +// that cannot be pulled, likely because they don't exist in the registry in +// the first place. This happens e.g. when the user tags an image locally +// themselves or when using docker-compose. +const NONEXISTENT_REPO: &str = "repository does not exist"; + +/// Returns a Vector of all containers, with Strings in the format +/// "REGISTRY/[PATH/]CONTAINER_NAME:TAG" +fn list_containers(crt: &Path) -> Result> { + debug!( + "Querying '{} images --format \"{{{{.Repository}}}}:{{{{.Tag}}}}\"' for containers", + crt.display() + ); + let output = Command::new(crt) + .args(&["images", "--format", "{{.Repository}}:{{.Tag}}"]) + .output()?; + let output_str = String::from_utf8(output.stdout)?; + + let mut retval = vec![]; + for line in output_str.lines() { + if line.starts_with("localhost") { + // Don't know how to update self-built containers + debug!("Skipping self-built container '{}'", line); + continue; + } + + if line.contains("") { + // Bogus/dangling container or intermediate layer + debug!("Skipping bogus container '{}'", line); + continue; + } + + debug!("Using container '{}'", line); + retval.push(String::from(line)); + } + + Ok(retval) +} + +pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { + // Prefer podman, fall back to docker if not present + let crt = require("podman").or_else(|_| require("docker"))?; + debug!("Using container runtime '{}'", crt.display()); + + print_separator("Containers"); + let mut success = true; + let containers = list_containers(&crt)?; + debug!("Containers to inspect: {:?}", containers); + + for container in containers.iter() { + debug!("Pulling container '{}'", container); + let args = vec!["pull", &container[..]]; + let mut exec = ctx.run_type().execute(&crt); + + if let Err(e) = exec.args(&args).check_run() { + error!("Pulling container '{}' failed: {}", container, e); + + // Find out if this is 'skippable' + // This is necessary e.g. for docker, because unlike podman docker doesn't tell from + // which repository a container originates (such as `docker.io`). This has the + // practical consequence that all containers, whether self-built, created by + // docker-compose or pulled from the docker hub, look exactly the same to us. We can + // only find out what went wrong by manually parsing the output of the command... + if match exec.check_output() { + Ok(s) => s.contains(NONEXISTENT_REPO), + Err(e) => match e.downcast_ref::() { + Some(TopgradeError::ProcessFailedWithOutput(_, stderr)) => stderr.contains(NONEXISTENT_REPO), + _ => false, + }, + } { + warn!("Skipping unknown container '{}'", container); + continue; + } + + success = false; + } + } + + if ctx.config().cleanup() { + // Remove dangling images + debug!("Removing dangling images"); + if let Err(e) = ctx.run_type().execute(&crt).args(&["image", "prune", "-f"]).check_run() { + error!("Removing dangling images failed: {}", e); + success = false; + } + } + + if success { + Ok(()) + } else { + Err(anyhow::anyhow!(error::StepFailed)) + } +} diff --git a/src/steps/mod.rs b/src/steps/mod.rs index 512dca95..b1b7e9ec 100644 --- a/src/steps/mod.rs +++ b/src/steps/mod.rs @@ -1,3 +1,4 @@ +pub mod containers; pub mod emacs; pub mod generic; pub mod git;