particle-os-cli/src/drivers/docker_driver.rs
Gerald Pinder 6e3a193e92
feat: Squash builds (#155)
### Buildah/Podman support

Buildah and podman can make heavy use of the squash feature. Something
that I've noticed when trying to build from inside of a container,
requiring intermediate layers with mounts causes build times to
skyrocket. Build times are much faster when using the `--squash`
functionality (seen as `--layers=false`).

Here are the following results from my personal build using both squash
and non-squash functionality.

#### Squash upgrade:

```
$> rpm-ostree upgrade
Pulling manifest: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-laptop
Importing: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-laptop (digest: sha256:60f743ba322041918d302e7e7f10438c59502e19343c294064bacb676c8eb7b7)
ostree chunk layers already present: 65
custom layers already present: 3
custom layers needed: 1 (814.0 MB)
```

All changes appear to show as a single custom layer. Any small change
even at the end of the build appears to require completely downloading
the new layer (squash only squashes additional layers on top of the base
layer). This makes sense as layers cannot currently be downloaded by
diff.

#### Non-squash upgrade:

```
$> rpm-ostree upgrade
Pulling manifest: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-desktop-gaming:latest
Importing: ostree-image-signed:docker://registry.gitlab.com/wunker-bunker/wunker-os/jp-desktop-gaming:latest (digest: sha256:0658b51febfcbaa1722961b7a6d2b197d3823a6228e330f45dd1e1aaefd145c5)
ostree chunk layers already present: 65
custom layers already present: 4
custom layers needed: 15 (942.4 MB)
```

As expected, there are more layers when not squashing and the size is
slightly bigger. Most likely due to there being extra information stored
in the layers that is subsequently removed.

### Docker support

Docker is apparently [no longer
supporting](https://github.com/docker/buildx/issues/1287) the use of the
`--squash` arg. The use of squash will not be available for the docker
driver in this case.
2024-04-11 19:15:30 +00:00

241 lines
7.2 KiB
Rust

use std::{
env,
process::{Command, Stdio},
};
use anyhow::{bail, Result};
use blue_build_utils::constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, SKOPEO_IMAGE};
use log::{info, trace, warn};
use semver::Version;
use serde::Deserialize;
use crate::image_metadata::ImageMetadata;
use super::{
credentials,
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, TagOpts},
BuildDriver, DriverVersion, InspectDriver,
};
#[derive(Debug, Deserialize)]
struct DockerVerisonJsonClient {
#[serde(alias = "Version")]
pub version: Version,
}
#[derive(Debug, Deserialize)]
struct DockerVersionJson {
#[serde(alias = "Client")]
pub client: DockerVerisonJsonClient,
}
#[derive(Debug)]
pub struct DockerDriver;
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 = Command::new("docker")
.arg("version")
.arg("-f")
.arg("json")
.output()?;
let version_json: DockerVersionJson = serde_json::from_slice(&output.stdout)?;
Ok(version_json.client.version)
}
}
impl BuildDriver for DockerDriver {
fn build(&self, 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 = Command::new("docker")
.arg("build")
.arg("-t")
.arg(opts.image.as_ref())
.arg("-f")
.arg(CONTAINER_FILE)
.arg(".")
.status()?;
if status.success() {
info!("Successfully built {}", opts.image);
} else {
bail!("Failed to build {}", opts.image);
}
Ok(())
}
fn tag(&self, opts: &TagOpts) -> Result<()> {
trace!("DockerDriver::tag({opts:#?})");
trace!("docker tag {} {}", opts.src_image, opts.dest_image);
let status = Command::new("docker")
.arg("tag")
.arg(opts.src_image.as_ref())
.arg(opts.dest_image.as_ref())
.status()?;
if status.success() {
info!("Successfully tagged {}!", opts.dest_image);
} else {
bail!("Failed to tag image {}", opts.dest_image);
}
Ok(())
}
fn push(&self, opts: &PushOpts) -> Result<()> {
trace!("DockerDriver::push({opts:#?})");
trace!("docker push {}", opts.image);
let status = Command::new("docker")
.arg("push")
.arg(opts.image.as_ref())
.status()?;
if status.success() {
info!("Successfully pushed {}!", opts.image);
} else {
bail!("Failed to push image {}", opts.image);
}
Ok(())
}
fn login(&self) -> Result<()> {
trace!("DockerDriver::login()");
let (registry, username, password) =
credentials::get().map(|c| (&c.registry, &c.username, &c.password))?;
trace!("docker login -u {username} -p [MASKED] {registry}");
let output = Command::new("docker")
.arg("login")
.arg("-u")
.arg(username)
.arg("-p")
.arg(password)
.arg(registry)
.output()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for docker: {err_out}");
}
Ok(())
}
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
trace!("DockerDriver::build_tag_push({opts:#?})");
if opts.squash {
warn!("Squash is deprecated for docker so this build will not squash");
}
let mut command = Command::new("docker");
trace!("docker buildx build -f {CONTAINER_FILE}");
command
.arg("buildx")
.arg("build")
.arg("-f")
.arg(CONTAINER_FILE);
// 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") {
trace!("--cache-from type=gha --cache-to type=gha");
command
.arg("--cache-from")
.arg("type=gha")
.arg("--cache-to")
.arg("type=gha");
}
match (opts.image.as_ref(), opts.archive_path.as_ref()) {
(Some(image), None) => {
if opts.tags.is_empty() {
trace!("-t {image}");
command.arg("-t").arg(image.as_ref());
} else {
for tag in opts.tags.as_ref() {
let full_image = format!("{image}:{tag}");
trace!("-t {full_image}");
command.arg("-t").arg(full_image);
}
}
if opts.push {
trace!("--output type=image,name={image},push=true,compression={},oci-mediatypes=true", opts.compression);
command.arg("--output").arg(format!(
"type=image,name={image},push=true,compression={},oci-mediatypes=true",
opts.compression
));
} else {
trace!("--load");
command.arg("--load");
}
}
(None, Some(archive_path)) => {
trace!("--output type=oci,dest={archive_path}");
command
.arg("--output")
.arg(format!("type=oci,dest={archive_path}"));
}
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
(None, None) => bail!("Need either the image or archive path set"),
}
trace!(".");
command.arg(".");
if command.status()?.success() {
if opts.push {
info!("Successfully built and pushed image");
} else {
info!("Successfully built image");
}
} else {
bail!("Failed to build image");
}
Ok(())
}
}
impl InspectDriver for DockerDriver {
fn get_metadata(&self, opts: &GetMetadataOpts) -> Result<ImageMetadata> {
trace!("DockerDriver::get_labels({opts:#?})");
let url = opts.tag.as_ref().map_or_else(
|| format!("docker://{}", opts.image),
|tag| format!("docker://{}:{tag}", opts.image),
);
trace!("docker run {SKOPEO_IMAGE} inspect {url}");
let output = Command::new("docker")
.arg("run")
.arg("--rm")
.arg(SKOPEO_IMAGE)
.arg("inspect")
.arg(&url)
.stderr(Stdio::inherit())
.output()?;
if output.status.success() {
info!("Successfully inspected image {url}!");
} else {
bail!("Failed to inspect image {url}")
}
Ok(serde_json::from_slice(&output.stdout)?)
}
}