Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

twoliter: move docker version check to buildsys #442

Merged
merged 1 commit into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}
Loading