Skip to content

Commit

Permalink
Merge #794
Browse files Browse the repository at this point in the history
794: Refactor docker and cross-util commands. r=Emilgardis a=Alexhuszagh

Refactor the internals of the docker module and separate the cross-util commands from the actual binary. Will simplify the internals for adding remote container support later.

Co-authored-by: Alex Huszagh <ahuszagh@gmail.com>
  • Loading branch information
bors[bot] and Alexhuszagh authored Jun 14, 2022
2 parents 150c892 + 96246b2 commit 6a63e3a
Show file tree
Hide file tree
Showing 11 changed files with 702 additions and 485 deletions.
213 changes: 213 additions & 0 deletions src/bin/commands/images.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
use clap::Args;
use cross::CommandExt;

// known image prefixes, with their registry
// the docker.io registry can also be implicit
const GHCR_IO: &str = cross::docker::CROSS_IMAGE;
const RUST_EMBEDDED: &str = "rustembedded/cross:";
const DOCKER_IO: &str = "docker.io/rustembedded/cross:";
const IMAGE_PREFIXES: &[&str] = &[GHCR_IO, DOCKER_IO, RUST_EMBEDDED];

#[derive(Args, Debug)]
pub struct ListImages {
/// Provide verbose diagnostic output.
#[clap(short, long)]
pub verbose: bool,
/// Container engine (such as docker or podman).
#[clap(long)]
pub engine: Option<String>,
}

#[derive(Args, Debug)]
pub struct RemoveImages {
/// If not provided, remove all images.
pub targets: Vec<String>,
/// Remove images matching provided targets.
#[clap(short, long)]
pub verbose: bool,
/// Force removal of images.
#[clap(short, long)]
pub force: bool,
/// Remove local (development) images.
#[clap(short, long)]
pub local: bool,
/// Remove images. Default is a dry run.
#[clap(short, long)]
pub execute: bool,
/// Container engine (such as docker or podman).
#[clap(long)]
pub engine: Option<String>,
}

#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
struct Image {
repository: String,
tag: String,
// need to remove images by ID, not just tag
id: String,
}

impl Image {
fn name(&self) -> String {
format!("{}:{}", self.repository, self.tag)
}
}

fn parse_image(image: &str) -> Image {
// this cannot panic: we've formatted our image list as `${repo}:${tag} ${id}`
let (repository, rest) = image.split_once(':').unwrap();
let (tag, id) = rest.split_once(' ').unwrap();
Image {
repository: repository.to_string(),
tag: tag.to_string(),
id: id.to_string(),
}
}

fn is_cross_image(repository: &str) -> bool {
IMAGE_PREFIXES.iter().any(|i| repository.starts_with(i))
}

fn is_local_image(tag: &str) -> bool {
tag.starts_with("local")
}

fn get_cross_images(
engine: &cross::docker::Engine,
verbose: bool,
local: bool,
) -> cross::Result<Vec<Image>> {
let stdout = cross::docker::subcommand(engine, "images")
.arg("--format")
.arg("{{.Repository}}:{{.Tag}} {{.ID}}")
.run_and_get_stdout(verbose)?;

let mut images: Vec<Image> = stdout
.lines()
.map(parse_image)
.filter(|image| is_cross_image(&image.repository))
.filter(|image| local || !is_local_image(&image.tag))
.collect();
images.sort();

Ok(images)
}

// the old rustembedded targets had the following format:
// repository = (${registry}/)?rustembedded/cross
// tag = ${target}(-${version})?
// the last component must match `[A-Za-z0-9_-]` and
// we must have at least 3 components. the first component
// may contain other characters, such as `thumbv8m.main-none-eabi`.
fn rustembedded_target(tag: &str) -> String {
let is_target_char = |c: char| c == '_' || c.is_ascii_alphanumeric();
let mut components = vec![];
for (index, component) in tag.split('-').enumerate() {
if index <= 2 || (!component.is_empty() && component.chars().all(is_target_char)) {
components.push(component)
} else {
break;
}
}

components.join("-")
}

fn get_image_target(image: &Image) -> cross::Result<String> {
if let Some(stripped) = image.repository.strip_prefix(GHCR_IO) {
Ok(stripped.to_string())
} else if let Some(tag) = image.tag.strip_prefix(RUST_EMBEDDED) {
Ok(rustembedded_target(tag))
} else if let Some(tag) = image.tag.strip_prefix(DOCKER_IO) {
Ok(rustembedded_target(tag))
} else {
eyre::bail!("cannot get target for image {}", image.name())
}
}

pub fn list_images(
ListImages { verbose, .. }: ListImages,
engine: &cross::docker::Engine,
) -> cross::Result<()> {
get_cross_images(engine, verbose, true)?
.iter()
.for_each(|line| println!("{}", line.name()));

Ok(())
}

fn remove_images(
engine: &cross::docker::Engine,
images: &[&str],
verbose: bool,
force: bool,
execute: bool,
) -> cross::Result<()> {
let mut command = cross::docker::subcommand(engine, "rmi");
if force {
command.arg("--force");
}
command.args(images);
if execute {
command.run(verbose).map_err(Into::into)
} else {
println!("{:?}", command);
Ok(())
}
}

pub fn remove_all_images(
RemoveImages {
verbose,
force,
local,
execute,
..
}: RemoveImages,
engine: &cross::docker::Engine,
) -> cross::Result<()> {
let images = get_cross_images(engine, verbose, local)?;
let ids: Vec<&str> = images.iter().map(|i| i.id.as_ref()).collect();
remove_images(engine, &ids, verbose, force, execute)
}

pub fn remove_target_images(
RemoveImages {
targets,
verbose,
force,
local,
execute,
..
}: RemoveImages,
engine: &cross::docker::Engine,
) -> cross::Result<()> {
let images = get_cross_images(engine, verbose, local)?;
let mut ids = vec![];
for image in images.iter() {
let target = get_image_target(image)?;
if targets.contains(&target) {
ids.push(image.id.as_ref());
}
}
remove_images(engine, &ids, verbose, force, execute)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_rustembedded_target() {
let targets = [
"x86_64-unknown-linux-gnu",
"x86_64-apple-darwin",
"thumbv8m.main-none-eabi",
];
for target in targets {
let versioned = format!("{target}-0.2.1");
assert_eq!(rustembedded_target(target), target.to_string());
assert_eq!(rustembedded_target(&versioned), target.to_string());
}
}
}
3 changes: 3 additions & 0 deletions src/bin/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod images;

pub use self::images::*;
Loading

0 comments on commit 6a63e3a

Please sign in to comment.