Skip to content

Commit

Permalink
twoliter: move docker version check to buildsys
Browse files Browse the repository at this point in the history
Twoliter does not require access to the docker daemon for publication
workflows (e.g. publishing AMIs or SSM parameters.) To accomodate
environments which do not provide access to the docker daemon for these
workflows, we only make assertions about the system's docker daemon
before using `docker build`.
  • Loading branch information
cbgbt committed Jan 22, 2025
1 parent ab231c9 commit 44c024a
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 75 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tools/buildsys/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pipesys.workspace = true
rand = { workspace = true, features = ["std", "std_rng"] }
regex.workspace = true
reqwest = { workspace = true, features = ["blocking", "rustls-tls"] }
semver.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_plain.workspace = true
serde_json.workspace = true
Expand Down
80 changes: 80 additions & 0 deletions tools/buildsys/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use nonzero_ext::nonzero;
use pipesys::server::Server as PipesysServer;
use rand::Rng;
use regex::Regex;
use semver::{Comparator, Op, Prerelease, Version, VersionReq};
use sha2::{Digest, Sha512};
use snafu::{ensure, OptionExt, ResultExt};
use std::collections::HashSet;
Expand Down Expand Up @@ -89,6 +90,23 @@ lazy_static! {
.unwrap();
}

/*
Twoliter relies on minimum Dockerfile syntax 1.4.3, which is shipped in Docker 23.0.0 by default
We do not use explicit `syntax=` directives to avoid network connections during the build.
*/
lazy_static! {
static ref MINIMUM_DOCKER_VERSION: VersionReq = VersionReq {
comparators: [Comparator {
op: Op::GreaterEq,
major: 23,
minor: None,
patch: None,
pre: Prerelease::default(),
}]
.into()
};
}

static DOCKER_BUILD_MAX_ATTEMPTS: NonZeroU16 = nonzero!(10u16);

// Expected UID for privileged and unprivileged processes inside the build container.
Expand Down Expand Up @@ -558,6 +576,8 @@ impl DockerBuild {
}

pub(crate) fn build(&self) -> Result<()> {
check_docker_version()?;

env::set_current_dir(&self.root_dir).context(error::DirectoryChangeSnafu {
path: &self.root_dir,
})?;
Expand Down Expand Up @@ -753,6 +773,66 @@ enum Retry<'a> {

// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=

pub fn docker_server_version() -> Result<Version> {
let docker_version_out = cmd("docker", ["version", "--format", "{{.Server.Version}}"])
.stderr_to_stdout()
.stdout_capture()
.unchecked()
.run()
.context(error::CommandStartSnafu)?;
let version_str = String::from_utf8_lossy(&docker_version_out.stdout)
.trim()
.to_string();

Version::parse(&version_str).context(error::VersionParseSnafu { version_str })
}

fn check_docker_version() -> Result<()> {
let docker_version = docker_server_version()?;

snafu::ensure!(
MINIMUM_DOCKER_VERSION.matches(&docker_version),
error::DockerVersionRequirementSnafu {
installed_version: docker_version,
required_version: MINIMUM_DOCKER_VERSION.clone()
}
);

Ok(())
}

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

#[test]
fn test_docker_version_req_25_0_5_passes() {
let version = Version::parse("25.0.5").unwrap();
assert!(MINIMUM_DOCKER_VERSION.matches(&version))
}

#[test]
fn test_docker_version_req_27_1_4_passes() {
let version = Version::parse("27.1.4").unwrap();
assert!(MINIMUM_DOCKER_VERSION.matches(&version))
}

#[test]
fn test_docker_version_req_18_0_9_fails() {
let version = Version::parse("18.0.9").unwrap();
assert!(!MINIMUM_DOCKER_VERSION.matches(&version))
}

#[test]
fn test_docker_version_req_20_10_27_fails() {
let version = Version::parse("20.10.27").unwrap();
assert!(!MINIMUM_DOCKER_VERSION.matches(&version))
}
}

// =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^= =^..^=

/// Add secrets that might be needed for builds. Since most builds won't use
/// them, they are not automatically tracked for changes. If necessary, builds
/// can emit the relevant cargo directives for tracking in their build script.
Expand Down
16 changes: 16 additions & 0 deletions tools/buildsys/src/builder/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ pub(crate) enum Error {
#[snafu(display("Failed to execute command: 'docker {}'", args))]
DockerExecution { args: String },

#[snafu(display(
"The installed docker ('{}') does not meet the minimum version requirement ('{}')",
installed_version,
required_version
))]
DockerVersionRequirement {
installed_version: semver::Version,
required_version: semver::VersionReq,
},

#[snafu(display("Failed to change directory to '{}': {}", path.display(), source))]
DirectoryChange {
path: PathBuf,
Expand Down Expand Up @@ -91,6 +101,12 @@ pub(crate) enum Error {
VariantParse {
source: bottlerocket_variant::error::Error,
},

#[snafu(display("Failed to parse version string '{version_str}': {source}"))]
VersionParse {
source: semver::Error,
version_str: String,
},
}

pub(super) type Result<T> = std::result::Result<T, Error>;
24 changes: 0 additions & 24 deletions twoliter/src/docker/commands.rs

This file was deleted.

2 changes: 0 additions & 2 deletions twoliter/src/docker/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
mod commands;
mod image;

pub(crate) use self::image::ImageUri;
pub(crate) use commands::Docker;
49 changes: 0 additions & 49 deletions twoliter/src/preflight.rs
Original file line number Diff line number Diff line change
@@ -1,34 +1,13 @@
//! This module performs checks that the current environment is compatible with twoliter, as well
//! as any other "global" setup that must occur before the build process begins.
use anyhow::{ensure, Result};
use lazy_static::lazy_static;
use semver::{Comparator, Op, Prerelease, VersionReq};
use which::which_global;

use crate::docker::Docker;

const REQUIRED_TOOLS: &[&str] = &["docker", "gzip", "lz4"];

lazy_static! {
// Twoliter relies on minimum Dockerfile syntax 1.4.3, which is shipped in Docker 23.0.0 by default
// We do not use explicit `syntax=` directives to avoid network connections during the build.
static ref MINIMUM_DOCKER_VERSION: VersionReq = VersionReq {
comparators: [
Comparator {
op: Op::GreaterEq,
major: 23,
minor: None,
patch: None,
pre: Prerelease::default(),
}
].into()
};
}

/// Runs all common setup required for twoliter.
///
/// * Ensures that any required system tools are installed an accessible.
/// * Sets up interrupt handler to cleanup on SIGINT
pub(crate) async fn preflight() -> Result<()> {
check_environment().await?;

Expand All @@ -37,7 +16,6 @@ pub(crate) async fn preflight() -> Result<()> {

pub(crate) async fn check_environment() -> Result<()> {
check_for_required_tools()?;
check_docker_version().await?;

Ok(())
}
Expand All @@ -51,30 +29,3 @@ fn check_for_required_tools() -> Result<()> {
}
Ok(())
}

async fn check_docker_version() -> Result<()> {
let docker_version = Docker::server_version().await?;

ensure!(
MINIMUM_DOCKER_VERSION.matches(&docker_version),
"docker found in PATH does not meet the minimum version requirements for twoliter: {}",
MINIMUM_DOCKER_VERSION.to_string(),
);

Ok(())
}

#[cfg(test)]
mod test {
use super::*;
use semver::Version;
use test_case::test_case;

#[test_case(Version::parse("25.0.5").unwrap(), true; "25.0.5 passes")]
#[test_case(Version::parse("27.1.4").unwrap(), true; "27.1.4 passes")]
#[test_case(Version::parse("18.0.9").unwrap(), false; "18.0.9 fails")]
#[test_case(Version::parse("20.10.27").unwrap(), false)]
fn test_docker_version_req(version: Version, is_ok: bool) {
assert_eq!(MINIMUM_DOCKER_VERSION.matches(&version), is_ok)
}
}

0 comments on commit 44c024a

Please sign in to comment.