Update Docker output parsing (#5996)

Docker output is not really stable across releases. This PR adds usage of the `--quiet` flag, so it ends up in something that is effectively stable.
Unfortunately, we cannot use the `--format,` as it does not work on Windows.
This commit is contained in:
Michał Wawrzyniec Urbańczyk 2023-03-19 21:39:45 +01:00 committed by GitHub
parent 201358e5cc
commit 130fd803f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -6,9 +6,25 @@ use crate::extensions::child::ChildExt;
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::Formatter; use std::fmt::Formatter;
use std::process::Stdio; use std::process::Stdio;
use std::str::FromStr;
/// Get the docker image identifier from the `docker build` command output.
///
/// This assumes format compatible with `--quiet` flag.
#[context("Failed to get the image identifier from the output: {output:?}.")]
fn get_image_id_from_build_output(output: &std::process::Output) -> Result<ImageId> {
trace!("Output: {:?}", output);
let built_image_id = std::str::from_utf8(&output.stdout)?
.lines()
.inspect(|line| debug!("{}", line))
.filter(|line| !line.is_empty())
.last()
.with_context(|| "Docker provided no output.")?
.split(' ')
.last()
.with_context(|| "The last line has no space!")?;
debug!("Image {} successfully built!", built_image_id);
ImageId::from_str(built_image_id)
}
#[derive(Clone, Debug, PartialEq, Ord, PartialOrd, Eq, Hash)] #[derive(Clone, Debug, PartialEq, Ord, PartialOrd, Eq, Hash)]
pub enum NetworkDriver { pub enum NetworkDriver {
@ -99,18 +115,8 @@ impl Docker {
command.arg("build").args(options.args()); command.arg("build").args(options.args());
debug!("{:?}", command); debug!("{:?}", command);
let output = command.output_ok().await?; let output = command.output_ok().await?;
trace!("Output: {:?}", output); let built_image_id = get_image_id_from_build_output(&output)?;
let built_image_id = std::str::from_utf8(&output.stdout)? Ok(built_image_id)
.lines()
.inspect(|line| debug!("{}", line))
.filter(|line| !line.is_empty())
.last()
.ok_or_else(|| anyhow!("Docker provided no output"))?
.split(' ')
.last()
.ok_or_else(|| anyhow!("The last line has no space!"))?;
debug!("Image {} successfully built!", built_image_id);
Ok(ImageId(built_image_id.into()))
} }
pub fn run_cmd(&self, options: &RunOptions) -> Result<Command> { pub fn run_cmd(&self, options: &RunOptions) -> Result<Command> {
@ -281,6 +287,35 @@ impl BuildOptions {
} }
} }
/// Get the environment variable and pass its value to docker build as a build argument.
///
/// Build argument name will be the same as the environment variable name.
/// If the environment variable is not set, an error is raised.
/// ```
/// use ide_ci::define_env_var;
/// use ide_ci::prelude::*;
/// use ide_ci::programs::docker::BuildOptions;
/// define_env_var! {DOCKER_USERNAME, String;};
/// let mut options = BuildOptions::new(".");
///
/// // Variable is not set, so this will fail.
/// assert!(options.add_build_arg_from_env(DOCKER_USERNAME).is_err());
/// assert!(options.build_args.is_empty());
///
/// // Set the variable and try again.
/// DOCKER_USERNAME.set("my_username".into()).unwrap();
/// options.add_build_arg_from_env(DOCKER_USERNAME).unwrap();
/// assert_eq!(options.build_args.get("DOCKER_USERNAME"), Some(&Some("my_username".into())));
/// ```
pub fn add_build_arg_from_env(
&mut self,
variable: impl crate::env::accessor::RawVariable,
) -> Result {
let value = variable.get_raw()?;
self.build_args.insert(variable.name().into(), Some(value));
Ok(())
}
pub fn add_build_arg_from_env_or<R>( pub fn add_build_arg_from_env_or<R>(
&mut self, &mut self,
name: impl AsRef<str>, name: impl AsRef<str>,
@ -300,6 +335,7 @@ impl BuildOptions {
pub fn args(&self) -> Vec<OsString> { pub fn args(&self) -> Vec<OsString> {
let mut ret = Vec::new(); let mut ret = Vec::new();
ret.push(self.context.clone().into()); ret.push(self.context.clone().into());
ret.push("--quiet".into());
if let Some(target) = self.target.as_ref() { if let Some(target) = self.target.as_ref() {
ret.push("--target".into()); ret.push("--target".into());
ret.push(target.clone()); ret.push(target.clone());
@ -516,13 +552,21 @@ impl RunOptions {
} }
} }
#[derive(Clone, Display, Debug)] #[derive(Clone, Display, Debug, PartialEq, Eq, Hash)]
pub struct ImageId(pub String); pub struct ImageId(pub String);
impl std::str::FromStr for ImageId {
type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
Ok(ImageId(s.into()))
}
}
#[derive(Clone, Debug, Display, Deref, AsRef)] #[derive(Clone, Debug, Display, Deref, AsRef)]
pub struct ContainerId(pub String); pub struct ContainerId(pub String);
impl FromStr for ContainerId { impl std::str::FromStr for ContainerId {
type Err = anyhow::Error; type Err = anyhow::Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
@ -534,6 +578,44 @@ impl FromStr for ContainerId {
mod tests { mod tests {
use super::*; use super::*;
/// Get the OS kernel version.
///
/// For Windows, the returned value is the build number, e.g. 20348 for Windows Server 2022
/// 21H2.
// This function might be unused, depending on what platform-specific tests are compiled.
#[allow(dead_code)]
fn get_kernel_version() -> Result<u32> {
let mut sysinfo = sysinfo::System::new();
sysinfo.refresh_all();
let ret = sysinfo
.kernel_version()
.with_context(|| "Failed to get OS kernel version.")?
.parse2()?;
debug!("OS kernel version: {ret}.");
Ok(ret)
}
/// See the tag listing on https://hub.docker.com/_/microsoft-windows-servercore
// This function might be unused, depending on what platform-specific tests are compiled.
#[allow(dead_code)]
fn get_windows_image_tag(kernel_version: u32) -> Result<&'static str> {
Ok(match kernel_version {
20348..=u32::MAX => "ltsc2022",
17763..=20347 => "ltsc2019",
14393..=17762 => "ltsc2016",
_ => anyhow::bail!("Unsupported OS kernel version: {kernel_version}."),
})
}
/// Provide a Windows image tag for the OS that we are running on.
///
/// See also: [`get_windows_image_tag`].
fn get_windows_image_tag_for_local() -> Result<&'static str> {
let kernel_version = get_kernel_version()?;
get_windows_image_tag(kernel_version)
}
#[tokio::test] #[tokio::test]
#[ignore] #[ignore]
async fn network() -> Result { async fn network() -> Result {
@ -550,4 +632,41 @@ mod tests {
dbg!(Docker.build(opts).await?); dbg!(Docker.build(opts).await?);
Ok(()) Ok(())
} }
#[tokio::test]
async fn build_test_linux() -> Result {
setup_logging()?;
let temp = tempfile::tempdir()?;
if Docker.lookup().is_err() {
info!("Docker not found, skipping test.");
return Ok(());
}
let dockerfile_text = match TARGET_OS {
OS::Linux => r#"
FROM ubuntu:22.04
RUN echo "Hello, world!"
"#
.to_string(),
OS::Windows => {
let tag = get_windows_image_tag_for_local()?;
format!(
r#"
FROM mcr.microsoft.com/windows/nanoserver:{tag}
RUN cmd /c "echo Hello World"
"#
)
}
_ => {
info!("Unsupported OS, skipping test.");
return Ok(());
}
};
let dockerfile_path = temp.path().join("Dockerfile");
crate::fs::tokio::write(&dockerfile_path, dockerfile_text).await?;
let opts = BuildOptions::new(temp.path());
// Make sure that the build succeeds and ID is returned.
let _id = Docker.build(opts).await?;
Ok(())
}
} }