particle-os-cli/process/drivers/docker_driver.rs
2024-11-26 21:21:26 -05:00

441 lines
13 KiB
Rust

use std::{
env,
io::Write,
path::Path,
process::{Command, ExitStatus, Stdio},
sync::Mutex,
};
use blue_build_utils::{
cmd,
constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, DOCKER_HOST},
credentials::Credentials,
string_vec,
};
use cached::proc_macro::cached;
use log::{debug, info, trace, warn};
use miette::{bail, miette, IntoDiagnostic, Result};
use once_cell::sync::Lazy;
use semver::Version;
use serde::Deserialize;
use tempfile::TempDir;
mod metadata;
use crate::{
drivers::{
opts::{
BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, RunOptsEnv,
RunOptsVolume, TagOpts,
},
traits::{BuildDriver, DriverVersion, InspectDriver, RunDriver},
types::ImageMetadata,
types::Platform,
},
logging::CommandLogging,
signal_handler::{add_cid, remove_cid, ContainerId, ContainerRuntime},
};
#[derive(Debug, Deserialize)]
struct DockerVerisonJsonClient {
#[serde(alias = "Version")]
pub version: Version,
}
#[derive(Debug, Deserialize)]
struct DockerVersionJson {
#[serde(alias = "Client")]
pub client: DockerVerisonJsonClient,
}
static DOCKER_SETUP: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
#[derive(Debug)]
pub struct DockerDriver;
impl DockerDriver {
fn setup() -> Result<()> {
trace!("DockerDriver::setup()");
let mut lock = DOCKER_SETUP.lock().expect("Should lock");
if *lock {
drop(lock);
return Ok(());
}
trace!("docker buildx ls --format={}", "{{.Name}}");
let ls_out = cmd!("docker", "buildx", "ls", "--format={{.Name}}")
.output()
.into_diagnostic()?;
if !ls_out.status.success() {
bail!("{}", String::from_utf8_lossy(&ls_out.stderr));
}
let ls_out = String::from_utf8(ls_out.stdout).into_diagnostic()?;
trace!("{ls_out}");
if !ls_out.lines().any(|line| line == "bluebuild") {
trace!("docker buildx create --bootstrap --driver=docker-container --name=bluebuild");
let create_out = cmd!(
"docker",
"buildx",
"create",
"--bootstrap",
"--driver=docker-container",
"--name=bluebuild",
)
.output()
.into_diagnostic()?;
if !create_out.status.success() {
bail!("{}", String::from_utf8_lossy(&create_out.stderr));
}
}
*lock = true;
drop(lock);
Ok(())
}
}
impl DriverVersion for DockerDriver {
// First docker verison to use buildkit
// https://docs.docker.com/build/buildkit/
const VERSION_REQ: &'static str = ">=23";
fn version() -> Result<Version> {
let output = cmd!("docker", "version", "-f", "json")
.output()
.into_diagnostic()?;
let version_json: DockerVersionJson =
serde_json::from_slice(&output.stdout).into_diagnostic()?;
Ok(version_json.client.version)
}
}
impl BuildDriver for DockerDriver {
fn build(opts: &BuildOpts) -> Result<()> {
trace!("DockerDriver::build({opts:#?})");
if opts.squash {
warn!("Squash is deprecated for docker so this build will not squash");
}
trace!("docker build -t {} -f {CONTAINER_FILE} .", opts.image);
let status = cmd!(
"docker",
"build",
if !matches!(opts.platform, Platform::Native) => [
"--platform",
opts.platform.to_string(),
],
"-t",
&*opts.image,
"-f",
&*opts.containerfile,
".",
)
.status()
.into_diagnostic()?;
if status.success() {
info!("Successfully built {}", opts.image);
} else {
bail!("Failed to build {}", opts.image);
}
Ok(())
}
fn tag(opts: &TagOpts) -> Result<()> {
trace!("DockerDriver::tag({opts:#?})");
trace!("docker tag {} {}", opts.src_image, opts.dest_image);
let status = cmd!("docker", "tag", &*opts.src_image, &*opts.dest_image,)
.status()
.into_diagnostic()?;
if status.success() {
info!("Successfully tagged {}!", opts.dest_image);
} else {
bail!("Failed to tag image {}", opts.dest_image);
}
Ok(())
}
fn push(opts: &PushOpts) -> Result<()> {
trace!("DockerDriver::push({opts:#?})");
trace!("docker push {}", opts.image);
let status = cmd!("docker", "push", &*opts.image)
.status()
.into_diagnostic()?;
if status.success() {
info!("Successfully pushed {}!", opts.image);
} else {
bail!("Failed to push image {}", opts.image);
}
Ok(())
}
fn login() -> Result<()> {
trace!("DockerDriver::login()");
if let Some(Credentials {
registry,
username,
password,
}) = Credentials::get()
{
let mut command = cmd!(
"docker",
"login",
"-u",
username,
"--password-stdin",
registry,
stdin = Stdio::piped(),
stdout = Stdio::piped(),
stderr = Stdio::piped(),
);
trace!("{command:?}");
let mut child = command.spawn().into_diagnostic()?;
write!(
child
.stdin
.as_mut()
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
"{password}"
)
.into_diagnostic()?;
let output = child.wait_with_output().into_diagnostic()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for docker:\n{}", err_out.trim());
}
debug!("Logged into {registry}");
}
Ok(())
}
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<Vec<String>> {
trace!("DockerDriver::build_tag_push({opts:#?})");
if opts.squash {
warn!("Squash is deprecated for docker so this build will not squash");
}
let mut command = cmd!(
"docker",
"buildx",
|command|? {
if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) {
Self::setup()?;
cmd!(command, "--builder=bluebuild");
}
},
"build",
"--pull",
if !matches!(opts.platform, Platform::Native) => [
"--platform",
opts.platform.to_string(),
],
"-f",
&*opts.containerfile,
// https://github.com/moby/buildkit?tab=readme-ov-file#github-actions-cache-experimental
if env::var(BB_BUILDKIT_CACHE_GHA)
.map_or_else(|_| false, |e| e == "true") => [
"--cache-from",
"type=gha",
"--cache-to",
"type=gha",
],
);
let final_images = match (opts.image.as_deref(), opts.archive_path.as_deref()) {
(Some(image), None) => {
let images = if opts.tags.is_empty() {
cmd!(command, "-t", image);
string_vec![image]
} else {
opts.tags.iter().for_each(|tag| {
cmd!(command, "-t", format!("{image}:{tag}"));
});
opts.tags
.iter()
.map(|tag| format!("{image}:{tag}"))
.collect()
};
let first_image = images.first().unwrap();
if opts.push {
cmd!(
command,
"--output",
format!(
"type=image,name={first_image},push=true,compression={},oci-mediatypes=true",
opts.compression
),
);
} else {
cmd!(command, "--load");
}
images
}
(None, Some(archive_path)) => {
cmd!(command, "--output", format!("type=oci,dest={archive_path}"));
string_vec![archive_path]
}
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
(None, None) => bail!("Need either the image or archive path set"),
};
let display_image = final_images.first().unwrap(); // There will always be at least one image
cmd!(command, ".");
trace!("{command:?}");
if command
.status_image_ref_progress(display_image, "Building Image")
.into_diagnostic()?
.success()
{
if opts.push {
info!("Successfully built and pushed image {}", display_image);
} else {
info!("Successfully built image {}", display_image);
}
} else {
bail!("Failed to build image {}", display_image);
}
Ok(final_images)
}
}
impl InspectDriver for DockerDriver {
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
get_metadata_cache(opts)
}
}
#[cached(
result = true,
key = "String",
convert = r#"{ format!("{}-{:?}-{}", &*opts.image, opts.tag.as_ref(), opts.platform)}"#,
sync_writes = true
)]
fn get_metadata_cache(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("DockerDriver::get_metadata({opts:#?})");
let url = opts.tag.as_ref().map_or_else(
|| format!("{}", opts.image),
|tag| format!("{}:{tag}", opts.image),
);
let mut command = cmd!(
"docker",
"buildx",
|command|? {
if !env::var(DOCKER_HOST).is_ok_and(|dh| !dh.is_empty()) {
DockerDriver::setup()?;
cmd!(command, "--builder=bluebuild");
}
},
"imagetools",
"inspect",
"--format",
"{{json .}}",
&url
);
trace!("{command:?}");
let output = command.output().into_diagnostic()?;
if output.status.success() {
info!("Successfully inspected image {url}!");
} else {
bail!("Failed to inspect image {url}")
}
serde_json::from_slice::<metadata::Metadata>(&output.stdout)
.into_diagnostic()
.inspect(|metadata| trace!("{metadata:#?}"))
.and_then(|metadata| ImageMetadata::try_from((metadata, opts.platform)))
.inspect(|metadata| trace!("{metadata:#?}"))
}
impl RunDriver for DockerDriver {
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
trace!("DockerDriver::run({opts:#?})");
let cid_path = TempDir::new()?;
let cid_file = cid_path.path().join("cid");
let cid = ContainerId::new(&cid_file, ContainerRuntime::Docker, false);
add_cid(&cid);
let status = docker_run(opts, &cid_file)
.status_image_ref_progress(&*opts.image, "Running container")?;
remove_cid(&cid);
Ok(status)
}
fn run_output(opts: &RunOpts) -> std::io::Result<std::process::Output> {
trace!("DockerDriver::run({opts:#?})");
let cid_path = TempDir::new()?;
let cid_file = cid_path.path().join("cid");
let cid = ContainerId::new(&cid_file, ContainerRuntime::Docker, false);
add_cid(&cid);
let output = docker_run(opts, &cid_file).output()?;
remove_cid(&cid);
Ok(output)
}
}
fn docker_run(opts: &RunOpts, cid_file: &Path) -> Command {
let command = cmd!(
"docker",
"run",
"--cidfile",
cid_file,
if opts.privileged => "--privileged",
if opts.remove => "--rm",
if opts.pull => "--pull=always",
for RunOptsVolume { path_or_vol_name, container_path } in opts.volumes.iter() => [
"--volume",
format!("{path_or_vol_name}:{container_path}"),
],
for RunOptsEnv { key, value } in opts.env_vars.iter() => [
"--env",
format!("{key}={value}"),
],
|command| {
match (opts.uid, opts.gid) {
(Some(uid), None) => cmd!(command, "-u", format!("{uid}")),
(Some(uid), Some(gid)) => cmd!(command, "-u", format!("{}:{}", uid, gid)),
_ => {}
}
},
&*opts.image,
for arg in opts.args.iter() => &**arg,
);
trace!("{command:?}");
command
}