refactor: Create SigningDriver and CiDriver (#197)
This also includes a new `login` command. The signing and CI logic is now using the Driver trait system along with a new experimental sigstore signing driver. New static macros have also been created to make implementation management easier for `Command` usage and `Driver` trait implementation calls. --------- Co-authored-by: xyny <60004820+xynydev@users.noreply.github.com>
This commit is contained in:
parent
3ecb0d3d93
commit
8ce83ba7ff
63 changed files with 6468 additions and 2083 deletions
158
process/drivers/buildah_driver.rs
Normal file
158
process/drivers/buildah_driver.rs
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
use std::{io::Write, process::Stdio};
|
||||
|
||||
use blue_build_utils::{cmd, credentials::Credentials};
|
||||
use log::{debug, error, info, trace};
|
||||
use miette::{bail, miette, IntoDiagnostic, Result};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::logging::CommandLogging;
|
||||
|
||||
use super::{
|
||||
opts::{BuildOpts, PushOpts, TagOpts},
|
||||
BuildDriver, DriverVersion,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct BuildahVersionJson {
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BuildahDriver;
|
||||
|
||||
impl DriverVersion for BuildahDriver {
|
||||
// RUN mounts for bind, cache, and tmpfs first supported in 1.24.0
|
||||
// https://buildah.io/releases/#changes-for-v1240
|
||||
const VERSION_REQ: &'static str = ">=1.24";
|
||||
|
||||
fn version() -> Result<Version> {
|
||||
trace!("BuildahDriver::version()");
|
||||
|
||||
trace!("buildah version --json");
|
||||
let output = cmd!("buildah", "version", "--json")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
let version_json: BuildahVersionJson = serde_json::from_slice(&output.stdout)
|
||||
.inspect_err(|e| error!("{e}: {}", String::from_utf8_lossy(&output.stdout)))
|
||||
.into_diagnostic()?;
|
||||
trace!("{version_json:#?}");
|
||||
|
||||
Ok(version_json.version)
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildDriver for BuildahDriver {
|
||||
fn build(opts: &BuildOpts) -> Result<()> {
|
||||
trace!("BuildahDriver::build({opts:#?})");
|
||||
|
||||
let command = cmd!(
|
||||
"buildah",
|
||||
"build",
|
||||
"--pull=true",
|
||||
format!("--layers={}", !opts.squash),
|
||||
"-f",
|
||||
&*opts.containerfile,
|
||||
"-t",
|
||||
&*opts.image,
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Building Image")
|
||||
.into_diagnostic()?;
|
||||
|
||||
if status.success() {
|
||||
info!("Successfully built {}", opts.image);
|
||||
} else {
|
||||
bail!("Failed to build {}", opts.image);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tag(opts: &TagOpts) -> Result<()> {
|
||||
trace!("BuildahDriver::tag({opts:#?})");
|
||||
|
||||
let mut command = cmd!("buildah", "tag", &*opts.src_image, &*opts.dest_image,);
|
||||
|
||||
trace!("{command:?}");
|
||||
if command.status().into_diagnostic()?.success() {
|
||||
info!("Successfully tagged {}!", opts.dest_image);
|
||||
} else {
|
||||
bail!("Failed to tag image {}", opts.dest_image);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push(opts: &PushOpts) -> Result<()> {
|
||||
trace!("BuildahDriver::push({opts:#?})");
|
||||
|
||||
let command = cmd!(
|
||||
"buildah",
|
||||
"push",
|
||||
format!(
|
||||
"--compression-format={}",
|
||||
opts.compression_type.unwrap_or_default()
|
||||
),
|
||||
&*opts.image,
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Pushing Image")
|
||||
.into_diagnostic()?;
|
||||
|
||||
if status.success() {
|
||||
info!("Successfully pushed {}!", opts.image);
|
||||
} else {
|
||||
bail!("Failed to push image {}", opts.image);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn login() -> Result<()> {
|
||||
trace!("BuildahDriver::login()");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = Credentials::get()
|
||||
{
|
||||
let mut command = cmd!(
|
||||
"buildah",
|
||||
"login",
|
||||
"-u",
|
||||
username,
|
||||
"--password-stdin",
|
||||
registry
|
||||
);
|
||||
command
|
||||
.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 buildah:\n{}", err_out.trim());
|
||||
}
|
||||
debug!("Logged into {registry}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
241
process/drivers/cosign_driver.rs
Normal file
241
process/drivers/cosign_driver.rs
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
use std::{fmt::Debug, fs, io::Write, path::Path, process::Stdio};
|
||||
|
||||
use blue_build_utils::{
|
||||
cmd,
|
||||
constants::{COSIGN_PASSWORD, COSIGN_PUB_PATH, COSIGN_YES},
|
||||
credentials::Credentials,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
use miette::{bail, miette, Context, IntoDiagnostic, Result};
|
||||
|
||||
use crate::drivers::opts::VerifyType;
|
||||
|
||||
use super::{
|
||||
functions::get_private_key,
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CosignDriver;
|
||||
|
||||
impl SigningDriver for CosignDriver {
|
||||
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"generate-key-pair",
|
||||
COSIGN_PASSWORD => "",
|
||||
COSIGN_YES => "true",
|
||||
);
|
||||
command.current_dir(path);
|
||||
|
||||
let status = command.status().into_diagnostic()?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("Failed to generate cosign key-pair!");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let priv_key = get_private_key(path)?;
|
||||
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"public-key",
|
||||
format!("--key={priv_key}"),
|
||||
COSIGN_PASSWORD => "",
|
||||
COSIGN_YES => "true",
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let output = command.output().into_diagnostic()?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Failed to run cosign public-key: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
|
||||
let found_pub_key = fs::read_to_string(path.join(COSIGN_PUB_PATH))
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
|
||||
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
|
||||
|
||||
if calculated_pub_key.trim() == found_pub_key.trim() {
|
||||
debug!("Cosign files match, continuing build");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
|
||||
}
|
||||
}
|
||||
|
||||
fn signing_login() -> Result<()> {
|
||||
trace!("CosignDriver::signing_login()");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = Credentials::get()
|
||||
{
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"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 cosign:\n{}", err_out.trim());
|
||||
}
|
||||
debug!("Logged into {registry}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sign(opts: &SignOpts) -> Result<()> {
|
||||
let image_digest: &str = opts.image.as_ref();
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"sign",
|
||||
if let Some(ref key) = opts.key => format!("--key={key}"),
|
||||
"--recursive",
|
||||
image_digest,
|
||||
COSIGN_PASSWORD => "",
|
||||
COSIGN_YES => "true",
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
if !command.status().into_diagnostic()?.success() {
|
||||
bail!("Failed to sign {image_digest}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify(opts: &VerifyOpts) -> Result<()> {
|
||||
let image_name_tag: &str = opts.image.as_ref();
|
||||
let mut command = cmd!(
|
||||
"cosign",
|
||||
"verify",
|
||||
|c| {
|
||||
match &opts.verify_type {
|
||||
VerifyType::File(path) => cmd!(c, format!("--key={}", path.display())),
|
||||
VerifyType::Keyless { issuer, identity } => cmd!(
|
||||
c,
|
||||
"--certificate-identity-regexp",
|
||||
identity as &str,
|
||||
"--certificate-oidc-issuer",
|
||||
issuer as &str,
|
||||
),
|
||||
};
|
||||
},
|
||||
image_name_tag
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
if !command.status().into_diagnostic()?.success() {
|
||||
bail!("Failed to verify {image_name_tag}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::drivers::{
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
|
||||
use super::CosignDriver;
|
||||
|
||||
#[test]
|
||||
fn generate_key_pair() {
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_key_pairs() {
|
||||
let path = Path::new("../test-files/keys");
|
||||
|
||||
let opts = CheckKeyPairOpts::builder().dir(path).build();
|
||||
|
||||
CosignDriver::check_signing_files(&opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "sigstore")]
|
||||
fn compatibility() {
|
||||
use crate::drivers::sigstore_driver::SigstoreDriver;
|
||||
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
}
|
||||
401
process/drivers/docker_driver.rs
Normal file
401
process/drivers/docker_driver.rs
Normal file
|
|
@ -0,0 +1,401 @@
|
|||
use std::{
|
||||
env,
|
||||
io::Write,
|
||||
path::Path,
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
sync::Mutex,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use blue_build_utils::{
|
||||
cmd,
|
||||
constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, DOCKER_HOST, SKOPEO_IMAGE},
|
||||
credentials::Credentials,
|
||||
string_vec,
|
||||
};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{debug, info, trace, warn};
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
use once_cell::sync::Lazy;
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::{
|
||||
drivers::image_metadata::ImageMetadata,
|
||||
logging::{CommandLogging, Logger},
|
||||
signal_handler::{add_cid, remove_cid, ContainerId, ContainerRuntime},
|
||||
};
|
||||
|
||||
use super::{
|
||||
opts::{BuildOpts, BuildTagPushOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
|
||||
BuildDriver, DriverVersion, InspectDriver, RunDriver,
|
||||
};
|
||||
|
||||
#[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",
|
||||
"-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().unwrap(), "{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<()> {
|
||||
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",
|
||||
"-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 mut final_image = String::new();
|
||||
|
||||
match (opts.image.as_deref(), opts.archive_path.as_deref()) {
|
||||
(Some(image), None) => {
|
||||
if opts.tags.is_empty() {
|
||||
final_image.push_str(image);
|
||||
cmd!(command, "-t", image);
|
||||
} else {
|
||||
final_image.push_str(
|
||||
format!("{image}:{}", opts.tags.first().map_or("", String::as_str))
|
||||
.as_str(),
|
||||
);
|
||||
|
||||
opts.tags.iter().for_each(|tag| {
|
||||
cmd!(command, "-t", format!("{image}:{tag}"));
|
||||
});
|
||||
}
|
||||
|
||||
if opts.push {
|
||||
cmd!(
|
||||
command,
|
||||
"--output",
|
||||
format!(
|
||||
"type=image,name={image},push=true,compression={},oci-mediatypes=true",
|
||||
opts.compression
|
||||
)
|
||||
);
|
||||
} else {
|
||||
cmd!(command, "--load");
|
||||
}
|
||||
}
|
||||
(None, Some(archive_path)) => {
|
||||
final_image.push_str(archive_path);
|
||||
|
||||
cmd!(command, "--output", 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"),
|
||||
}
|
||||
|
||||
cmd!(command, ".");
|
||||
|
||||
trace!("{command:?}");
|
||||
if command
|
||||
.status_image_ref_progress(&final_image, "Building Image")
|
||||
.into_diagnostic()?
|
||||
.success()
|
||||
{
|
||||
if opts.push {
|
||||
info!("Successfully built and pushed image {}", final_image);
|
||||
} else {
|
||||
info!("Successfully built image {}", final_image);
|
||||
}
|
||||
} else {
|
||||
bail!("Failed to build image {}", final_image);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectDriver for DockerDriver {
|
||||
fn get_metadata(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),
|
||||
);
|
||||
|
||||
let progress = Logger::multi_progress().add(
|
||||
ProgressBar::new_spinner()
|
||||
.with_style(ProgressStyle::default_spinner())
|
||||
.with_message(format!("Inspecting metadata for {url}")),
|
||||
);
|
||||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
let output = Self::run_output(
|
||||
&RunOpts::builder()
|
||||
.image(SKOPEO_IMAGE)
|
||||
.args(string_vec!["inspect", url.clone()])
|
||||
.remove(true)
|
||||
.build(),
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
|
||||
progress.finish();
|
||||
Logger::multi_progress().remove(&progress);
|
||||
|
||||
if output.status.success() {
|
||||
info!("Successfully inspected image {url}!");
|
||||
} else {
|
||||
bail!("Failed to inspect image {url}")
|
||||
}
|
||||
|
||||
serde_json::from_slice(&output.stdout).into_diagnostic()
|
||||
}
|
||||
}
|
||||
|
||||
impl RunDriver for DockerDriver {
|
||||
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
|
||||
let cid_path = TempDir::new("docker")?;
|
||||
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> {
|
||||
let cid_path = TempDir::new("docker")?;
|
||||
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 {
|
||||
cmd!(
|
||||
"docker",
|
||||
"run",
|
||||
format!("--cidfile={}", cid_file.display()),
|
||||
if opts.privileged => "--privileged",
|
||||
if opts.remove => "--rm",
|
||||
if opts.pull => "--pull=always",
|
||||
for volume in opts.volumes => [
|
||||
"--volume",
|
||||
format!("{}:{}", volume.path_or_vol_name, volume.container_path),
|
||||
],
|
||||
for env in opts.env_vars => [
|
||||
"--env",
|
||||
format!("{}={}", env.key, env.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 opts.args,
|
||||
)
|
||||
}
|
||||
52
process/drivers/functions.rs
Normal file
52
process/drivers/functions.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
use std::{env, path::Path};
|
||||
|
||||
use blue_build_utils::{
|
||||
constants::{BB_PRIVATE_KEY, COSIGN_PRIVATE_KEY, COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
|
||||
string,
|
||||
};
|
||||
use miette::{bail, Result};
|
||||
|
||||
use super::opts::PrivateKey;
|
||||
|
||||
pub(super) fn get_private_key<P>(path: P) -> Result<PrivateKey>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let path = path.as_ref();
|
||||
|
||||
Ok(
|
||||
match (
|
||||
path.join(COSIGN_PUB_PATH).exists(),
|
||||
env::var(BB_PRIVATE_KEY).ok(),
|
||||
env::var(COSIGN_PRIVATE_KEY).ok(),
|
||||
path.join(COSIGN_PRIV_PATH),
|
||||
) {
|
||||
(true, Some(private_key), _, _) if !private_key.is_empty() => {
|
||||
PrivateKey::Env(string!(BB_PRIVATE_KEY))
|
||||
}
|
||||
(true, _, Some(cosign_priv_key), _) if !cosign_priv_key.is_empty() => {
|
||||
PrivateKey::Env(string!(COSIGN_PRIVATE_KEY))
|
||||
}
|
||||
(true, _, _, cosign_priv_key_path) if cosign_priv_key_path.exists() => {
|
||||
PrivateKey::Path(cosign_priv_key_path)
|
||||
}
|
||||
_ => {
|
||||
bail!(
|
||||
help = format!(
|
||||
"{}{}{}{}{}{}",
|
||||
format_args!("Make sure you have a `{COSIGN_PUB_PATH}`\n"),
|
||||
format_args!(
|
||||
"in the root of your repo and have either {COSIGN_PRIVATE_KEY}\n"
|
||||
),
|
||||
format_args!("set in your env variables or a `{COSIGN_PRIV_PATH}`\n"),
|
||||
"file in the root of your repo.\n\n",
|
||||
"See https://blue-build.org/how-to/cosign/ for more information.\n\n",
|
||||
"If you don't want to sign your image, use the `--no-sign` flag.",
|
||||
),
|
||||
"{}",
|
||||
"Unable to find private/public key pair",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
272
process/drivers/github_driver.rs
Normal file
272
process/drivers/github_driver.rs
Normal file
|
|
@ -0,0 +1,272 @@
|
|||
use blue_build_utils::{
|
||||
constants::{
|
||||
GITHUB_EVENT_NAME, GITHUB_REF_NAME, GITHUB_SHA, GITHUB_TOKEN_ISSUER_URL,
|
||||
GITHUB_WORKFLOW_REF, PR_EVENT_NUMBER,
|
||||
},
|
||||
get_env_var,
|
||||
};
|
||||
use event::Event;
|
||||
use log::trace;
|
||||
|
||||
use super::{CiDriver, Driver};
|
||||
|
||||
mod event;
|
||||
|
||||
pub struct GithubDriver;
|
||||
|
||||
impl CiDriver for GithubDriver {
|
||||
fn on_default_branch() -> bool {
|
||||
Event::try_new().map_or_else(
|
||||
|_| false,
|
||||
|event| match (event.commit_ref, event.head) {
|
||||
(Some(commit_ref), _) => {
|
||||
commit_ref.trim_start_matches("refs/heads/") == event.repository.default_branch
|
||||
}
|
||||
(_, Some(head)) => event.repository.default_branch == head.commit_ref,
|
||||
_ => false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn keyless_cert_identity() -> miette::Result<String> {
|
||||
get_env_var(GITHUB_WORKFLOW_REF)
|
||||
}
|
||||
|
||||
fn oidc_provider() -> miette::Result<String> {
|
||||
Ok(GITHUB_TOKEN_ISSUER_URL.to_string())
|
||||
}
|
||||
|
||||
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
let os_version = Driver::get_os_version(recipe)?;
|
||||
let github_event_name = get_env_var(GITHUB_EVENT_NAME)?;
|
||||
|
||||
if github_event_name == "pull_request" {
|
||||
trace!("Running in a PR");
|
||||
|
||||
let github_event_number = get_env_var(PR_EVENT_NUMBER)?;
|
||||
|
||||
tags.push(format!("pr-{github_event_number}-{os_version}"));
|
||||
} else if Self::on_default_branch() {
|
||||
tags.push(os_version.to_string());
|
||||
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
tags.push(format!("{timestamp}-{os_version}"));
|
||||
|
||||
if let Some(ref alt_tags) = recipe.alt_tags {
|
||||
tags.extend(alt_tags.iter().map(ToString::to_string));
|
||||
} else {
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
}
|
||||
} else {
|
||||
let github_ref_name = get_env_var(GITHUB_REF_NAME)?;
|
||||
|
||||
tags.push(format!("br-{github_ref_name}-{os_version}"));
|
||||
}
|
||||
|
||||
let mut short_sha = get_env_var(GITHUB_SHA)?;
|
||||
short_sha.truncate(7);
|
||||
|
||||
tags.push(format!("{short_sha}-{os_version}"));
|
||||
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn get_repo_url() -> miette::Result<String> {
|
||||
Ok(Event::try_new()?.repository.html_url)
|
||||
}
|
||||
|
||||
fn get_registry() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"ghcr.io/{}",
|
||||
Event::try_new()?.repository.owner.login
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::env;
|
||||
|
||||
use blue_build_utils::constants::{
|
||||
GITHUB_EVENT_NAME, GITHUB_EVENT_PATH, GITHUB_REF_NAME, GITHUB_SHA, PR_EVENT_NUMBER,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drivers::CiDriver,
|
||||
test::{create_test_recipe, BB_UNIT_TEST_MOCK_GET_OS_VERSION, ENV_LOCK},
|
||||
};
|
||||
|
||||
use super::GithubDriver;
|
||||
|
||||
fn setup_default_branch() {
|
||||
setup();
|
||||
env::set_var(
|
||||
GITHUB_EVENT_PATH,
|
||||
"../test-files/github-events/default-branch.json",
|
||||
);
|
||||
env::set_var(GITHUB_REF_NAME, "main");
|
||||
}
|
||||
|
||||
fn setup_pr_branch() {
|
||||
setup();
|
||||
env::set_var(
|
||||
GITHUB_EVENT_PATH,
|
||||
"../test-files/github-events/pr-branch.json",
|
||||
);
|
||||
env::set_var(GITHUB_EVENT_NAME, "pull_request");
|
||||
env::set_var(GITHUB_REF_NAME, "test");
|
||||
env::set_var(PR_EVENT_NUMBER, "12");
|
||||
}
|
||||
|
||||
fn setup_branch() {
|
||||
setup();
|
||||
env::set_var(GITHUB_EVENT_PATH, "../test-files/github-events/branch.json");
|
||||
env::set_var(GITHUB_REF_NAME, "test");
|
||||
}
|
||||
|
||||
fn setup() {
|
||||
env::set_var(GITHUB_EVENT_NAME, "push");
|
||||
env::set_var(GITHUB_SHA, "1234567890");
|
||||
env::set_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION, "");
|
||||
}
|
||||
|
||||
fn teardown() {
|
||||
env::remove_var(GITHUB_EVENT_NAME);
|
||||
env::remove_var(GITHUB_EVENT_PATH);
|
||||
env::remove_var(GITHUB_REF_NAME);
|
||||
env::remove_var(PR_EVENT_NUMBER);
|
||||
env::remove_var(GITHUB_SHA);
|
||||
env::remove_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_registry() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let registry = GithubDriver::get_registry().unwrap();
|
||||
|
||||
assert_eq!(registry, "ghcr.io/test-owner");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_true() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
assert!(GithubDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_false() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_pr_branch();
|
||||
|
||||
assert!(!GithubDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_repo_url() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
let url = GithubDriver::get_repo_url().unwrap();
|
||||
|
||||
assert_eq!(url, "https://example.com/");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"latest".to_string(),
|
||||
timestamp,
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch_alt_tags() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut recipe = create_test_recipe();
|
||||
|
||||
recipe.alt_tags = Some(vec!["test-tag1".into(), "test-tag2".into()]);
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&recipe).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.extend(recipe.alt_tags.unwrap().iter().map(ToString::to_string));
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_pr_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_pr_branch();
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["pr-12-40".to_string(), "1234567-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
let mut tags = GithubDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["1234567-40".to_string(), "br-test-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
}
|
||||
44
process/drivers/github_driver/event.rs
Normal file
44
process/drivers/github_driver/event.rs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
use std::{fs, path::PathBuf};
|
||||
|
||||
use blue_build_utils::{constants::GITHUB_EVENT_PATH, get_env_var};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct Event {
|
||||
pub repository: EventRepository,
|
||||
// pub base: Option<EventRefInfo>,
|
||||
pub head: Option<EventRefInfo>,
|
||||
|
||||
#[serde(alias = "ref")]
|
||||
pub commit_ref: Option<String>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
pub fn try_new() -> Result<Self> {
|
||||
get_env_var(GITHUB_EVENT_PATH)
|
||||
.map(PathBuf::from)
|
||||
.and_then(|event_path| {
|
||||
serde_json::from_str::<Self>(&fs::read_to_string(event_path).into_diagnostic()?)
|
||||
.into_diagnostic()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct EventRepository {
|
||||
pub default_branch: String,
|
||||
pub owner: EventRepositoryOwner,
|
||||
pub html_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct EventRepositoryOwner {
|
||||
pub login: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub(super) struct EventRefInfo {
|
||||
#[serde(alias = "ref")]
|
||||
pub commit_ref: String,
|
||||
}
|
||||
293
process/drivers/gitlab_driver.rs
Normal file
293
process/drivers/gitlab_driver.rs
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
use std::env;
|
||||
|
||||
use blue_build_utils::{
|
||||
constants::{
|
||||
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
|
||||
CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_PROJECT_URL, CI_REGISTRY,
|
||||
CI_SERVER_HOST, CI_SERVER_PROTOCOL,
|
||||
},
|
||||
get_env_var,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::drivers::Driver;
|
||||
|
||||
use super::CiDriver;
|
||||
|
||||
pub struct GitlabDriver;
|
||||
|
||||
impl CiDriver for GitlabDriver {
|
||||
fn on_default_branch() -> bool {
|
||||
env::var(CI_DEFAULT_BRANCH).is_ok_and(|default_branch| {
|
||||
env::var(CI_COMMIT_REF_NAME).is_ok_and(|branch| default_branch == branch)
|
||||
})
|
||||
}
|
||||
|
||||
fn keyless_cert_identity() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}//.gitlab-ci.yml@refs/heads/{}",
|
||||
get_env_var(CI_PROJECT_URL)?,
|
||||
get_env_var(CI_DEFAULT_BRANCH)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn oidc_provider() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}://{}",
|
||||
get_env_var(CI_SERVER_PROTOCOL)?,
|
||||
get_env_var(CI_SERVER_HOST)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
|
||||
let mut tags: Vec<String> = Vec::new();
|
||||
let os_version = Driver::get_os_version(recipe)?;
|
||||
|
||||
if Self::on_default_branch() {
|
||||
debug!("Running on the default branch");
|
||||
|
||||
tags.push(os_version.to_string());
|
||||
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
tags.push(format!("{timestamp}-{os_version}"));
|
||||
|
||||
if let Some(ref alt_tags) = recipe.alt_tags {
|
||||
tags.extend(alt_tags.iter().map(ToString::to_string));
|
||||
} else {
|
||||
tags.push("latest".into());
|
||||
tags.push(timestamp);
|
||||
}
|
||||
} else if let Ok(mr_iid) = env::var(CI_MERGE_REQUEST_IID) {
|
||||
trace!("{CI_MERGE_REQUEST_IID}={mr_iid}");
|
||||
|
||||
let pipeline_source = get_env_var(CI_PIPELINE_SOURCE)?;
|
||||
trace!("{CI_PIPELINE_SOURCE}={pipeline_source}");
|
||||
|
||||
if pipeline_source == "merge_request_event" {
|
||||
debug!("Running in a MR");
|
||||
tags.push(format!("mr-{mr_iid}-{os_version}"));
|
||||
}
|
||||
} else {
|
||||
let commit_branch = get_env_var(CI_COMMIT_REF_NAME)?;
|
||||
trace!("{CI_COMMIT_REF_NAME}={commit_branch}");
|
||||
|
||||
debug!("Running on branch {commit_branch}");
|
||||
tags.push(format!("br-{commit_branch}-{os_version}"));
|
||||
}
|
||||
|
||||
let commit_sha = get_env_var(CI_COMMIT_SHORT_SHA)?;
|
||||
trace!("{CI_COMMIT_SHORT_SHA}={commit_sha}");
|
||||
|
||||
tags.push(format!("{commit_sha}-{os_version}"));
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
fn get_repo_url() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}://{}/{}/{}",
|
||||
get_env_var(CI_SERVER_PROTOCOL)?,
|
||||
get_env_var(CI_SERVER_HOST)?,
|
||||
get_env_var(CI_PROJECT_NAMESPACE)?,
|
||||
get_env_var(CI_PROJECT_NAME)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn get_registry() -> miette::Result<String> {
|
||||
Ok(format!(
|
||||
"{}/{}/{}",
|
||||
get_env_var(CI_REGISTRY)?,
|
||||
get_env_var(CI_PROJECT_NAMESPACE)?,
|
||||
get_env_var(CI_PROJECT_NAME)?,
|
||||
)
|
||||
.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::env;
|
||||
|
||||
use blue_build_utils::constants::{
|
||||
CI_COMMIT_REF_NAME, CI_COMMIT_SHORT_SHA, CI_DEFAULT_BRANCH, CI_MERGE_REQUEST_IID,
|
||||
CI_PIPELINE_SOURCE, CI_PROJECT_NAME, CI_PROJECT_NAMESPACE, CI_REGISTRY, CI_SERVER_HOST,
|
||||
CI_SERVER_PROTOCOL,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
drivers::CiDriver,
|
||||
test::{create_test_recipe, BB_UNIT_TEST_MOCK_GET_OS_VERSION, ENV_LOCK},
|
||||
};
|
||||
|
||||
use super::GitlabDriver;
|
||||
|
||||
fn setup_default_branch() {
|
||||
setup();
|
||||
env::set_var(CI_COMMIT_REF_NAME, "main");
|
||||
}
|
||||
|
||||
fn setup_mr_branch() {
|
||||
setup();
|
||||
env::set_var(CI_MERGE_REQUEST_IID, "12");
|
||||
env::set_var(CI_PIPELINE_SOURCE, "merge_request_event");
|
||||
env::set_var(CI_COMMIT_REF_NAME, "test");
|
||||
}
|
||||
|
||||
fn setup_branch() {
|
||||
setup();
|
||||
env::set_var(CI_COMMIT_REF_NAME, "test");
|
||||
}
|
||||
|
||||
fn setup() {
|
||||
env::set_var(CI_DEFAULT_BRANCH, "main");
|
||||
env::set_var(CI_COMMIT_SHORT_SHA, "1234567");
|
||||
env::set_var(CI_REGISTRY, "registry.example.com");
|
||||
env::set_var(CI_PROJECT_NAMESPACE, "test-project");
|
||||
env::set_var(CI_PROJECT_NAME, "test");
|
||||
env::set_var(CI_SERVER_PROTOCOL, "https");
|
||||
env::set_var(CI_SERVER_HOST, "gitlab.example.com");
|
||||
env::set_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION, "");
|
||||
}
|
||||
|
||||
fn teardown() {
|
||||
env::remove_var(CI_COMMIT_REF_NAME);
|
||||
env::remove_var(CI_MERGE_REQUEST_IID);
|
||||
env::remove_var(CI_PIPELINE_SOURCE);
|
||||
env::remove_var(CI_DEFAULT_BRANCH);
|
||||
env::remove_var(CI_COMMIT_SHORT_SHA);
|
||||
env::remove_var(CI_REGISTRY);
|
||||
env::remove_var(CI_PROJECT_NAMESPACE);
|
||||
env::remove_var(CI_PROJECT_NAME);
|
||||
env::remove_var(CI_SERVER_PROTOCOL);
|
||||
env::remove_var(CI_SERVER_HOST);
|
||||
env::remove_var(BB_UNIT_TEST_MOCK_GET_OS_VERSION);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_registry() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup();
|
||||
|
||||
let registry = GitlabDriver::get_registry().unwrap();
|
||||
|
||||
assert_eq!(registry, "registry.example.com/test-project/test");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_true() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
assert!(GitlabDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_default_branch_false() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
assert!(!GitlabDriver::on_default_branch());
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_repo_url() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup();
|
||||
|
||||
let url = GitlabDriver::get_repo_url().unwrap();
|
||||
|
||||
assert_eq!(url, "https://gitlab.example.com/test-project/test");
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"latest".to_string(),
|
||||
timestamp,
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_default_branch_alt_tags() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
let timestamp = blue_build_utils::get_tag_timestamp();
|
||||
|
||||
setup_default_branch();
|
||||
|
||||
let mut recipe = create_test_recipe();
|
||||
|
||||
recipe.alt_tags = Some(vec!["test-tag1".into(), "test-tag2".into()]);
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&recipe).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec![
|
||||
format!("{timestamp}-40"),
|
||||
"1234567-40".to_string(),
|
||||
"40".to_string(),
|
||||
];
|
||||
expected_tags.extend(recipe.alt_tags.unwrap().iter().map(ToString::to_string));
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_mr_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_mr_branch();
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["mr-12-40".to_string(), "1234567-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_tags_branch() {
|
||||
let _env = ENV_LOCK.lock().unwrap();
|
||||
|
||||
setup_branch();
|
||||
|
||||
let mut tags = GitlabDriver::generate_tags(&create_test_recipe()).unwrap();
|
||||
tags.sort();
|
||||
|
||||
let mut expected_tags = vec!["1234567-40".to_string(), "br-test-40".to_string()];
|
||||
expected_tags.sort();
|
||||
|
||||
assert_eq!(tags, expected_tags);
|
||||
|
||||
teardown();
|
||||
}
|
||||
}
|
||||
26
process/drivers/image_metadata.rs
Normal file
26
process/drivers/image_metadata.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
use blue_build_utils::constants::IMAGE_VERSION_LABEL;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ImageMetadata {
|
||||
#[serde(alias = "Labels")]
|
||||
pub labels: HashMap<String, Value>,
|
||||
|
||||
#[serde(alias = "Digest")]
|
||||
pub digest: String,
|
||||
}
|
||||
|
||||
impl ImageMetadata {
|
||||
#[must_use]
|
||||
pub fn get_version(&self) -> Option<u64> {
|
||||
Some(
|
||||
self.labels
|
||||
.get(IMAGE_VERSION_LABEL)?
|
||||
.as_str()
|
||||
.and_then(|v| lenient_semver::parse(v).ok())?
|
||||
.major,
|
||||
)
|
||||
}
|
||||
}
|
||||
43
process/drivers/local_driver.rs
Normal file
43
process/drivers/local_driver.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use log::trace;
|
||||
use miette::bail;
|
||||
|
||||
use super::{CiDriver, Driver};
|
||||
|
||||
pub struct LocalDriver;
|
||||
|
||||
impl CiDriver for LocalDriver {
|
||||
fn on_default_branch() -> bool {
|
||||
trace!("LocalDriver::on_default_branch()");
|
||||
false
|
||||
}
|
||||
|
||||
fn keyless_cert_identity() -> miette::Result<String> {
|
||||
trace!("LocalDriver::keyless_cert_identity()");
|
||||
bail!("Keyless not supported");
|
||||
}
|
||||
|
||||
fn oidc_provider() -> miette::Result<String> {
|
||||
trace!("LocalDriver::oidc_provider()");
|
||||
bail!("Keyless not supported");
|
||||
}
|
||||
|
||||
fn generate_tags(recipe: &blue_build_recipe::Recipe) -> miette::Result<Vec<String>> {
|
||||
trace!("LocalDriver::generate_tags({recipe:?})");
|
||||
Ok(vec![format!("local-{}", Driver::get_os_version(recipe)?)])
|
||||
}
|
||||
|
||||
fn generate_image_name(recipe: &blue_build_recipe::Recipe) -> miette::Result<String> {
|
||||
trace!("LocalDriver::generate_image_name({recipe:?})");
|
||||
Ok(recipe.name.trim().to_lowercase())
|
||||
}
|
||||
|
||||
fn get_repo_url() -> miette::Result<String> {
|
||||
trace!("LocalDriver::get_repo_url()");
|
||||
Ok(String::new())
|
||||
}
|
||||
|
||||
fn get_registry() -> miette::Result<String> {
|
||||
trace!("LocalDriver::get_registry()");
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
27
process/drivers/opts.rs
Normal file
27
process/drivers/opts.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
use clap::ValueEnum;
|
||||
|
||||
pub use build::*;
|
||||
pub use inspect::*;
|
||||
pub use run::*;
|
||||
pub use signing::*;
|
||||
|
||||
mod build;
|
||||
mod inspect;
|
||||
mod run;
|
||||
mod signing;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default, ValueEnum)]
|
||||
pub enum CompressionType {
|
||||
#[default]
|
||||
Gzip,
|
||||
Zstd,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CompressionType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Zstd => "zstd",
|
||||
Self::Gzip => "gzip",
|
||||
})
|
||||
}
|
||||
}
|
||||
83
process/drivers/opts/build.rs
Normal file
83
process/drivers/opts/build.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use std::{borrow::Cow, path::Path};
|
||||
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use super::CompressionType;
|
||||
|
||||
/// Options for building
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct BuildOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'a, str>,
|
||||
|
||||
#[builder(default)]
|
||||
pub squash: bool,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub containerfile: Cow<'a, Path>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct TagOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub src_image: Cow<'a, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub dest_image: Cow<'a, str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct PushOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'a, str>,
|
||||
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub compression_type: Option<CompressionType>,
|
||||
}
|
||||
|
||||
/// Options for building, tagging, and pusing images.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct BuildTagPushOpts<'a> {
|
||||
/// The base image name.
|
||||
///
|
||||
/// NOTE: You cannot have this set with `archive_path` set.
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub image: Option<Cow<'a, str>>,
|
||||
|
||||
/// The path to the archive file.
|
||||
///
|
||||
/// NOTE: You cannot have this set with image set.
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub archive_path: Option<Cow<'a, str>>,
|
||||
|
||||
/// The path to the Containerfile to build.
|
||||
#[builder(setter(into))]
|
||||
pub containerfile: Cow<'a, Path>,
|
||||
|
||||
/// The list of tags for the image being built.
|
||||
#[builder(default, setter(into))]
|
||||
pub tags: Cow<'a, [String]>,
|
||||
|
||||
/// Enable pushing the image.
|
||||
#[builder(default)]
|
||||
pub push: bool,
|
||||
|
||||
/// Enable retry logic for pushing.
|
||||
#[builder(default)]
|
||||
pub retry_push: bool,
|
||||
|
||||
/// Number of times to retry pushing.
|
||||
///
|
||||
/// Defaults to 1.
|
||||
#[builder(default = 1)]
|
||||
pub retry_count: u8,
|
||||
|
||||
/// The compression type to use when pushing.
|
||||
#[builder(default)]
|
||||
pub compression: CompressionType,
|
||||
|
||||
/// Run all steps in a single layer.
|
||||
#[builder(default)]
|
||||
pub squash: bool,
|
||||
}
|
||||
12
process/drivers/opts/inspect.rs
Normal file
12
process/drivers/opts/inspect.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct GetMetadataOpts<'a> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'a, str>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub tag: Option<Cow<'a, str>>,
|
||||
}
|
||||
82
process/drivers/opts/run.rs
Normal file
82
process/drivers/opts/run.rs
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub args: Cow<'scope, [String]>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub env_vars: Cow<'scope, [RunOptsEnv<'scope>]>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub volumes: Cow<'scope, [RunOptsVolume<'scope>]>,
|
||||
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub uid: Option<u32>,
|
||||
|
||||
#[builder(default, setter(strip_option))]
|
||||
pub gid: Option<u32>,
|
||||
|
||||
#[builder(default, setter(into))]
|
||||
pub workdir: Cow<'scope, str>,
|
||||
|
||||
#[builder(default)]
|
||||
pub privileged: bool,
|
||||
|
||||
#[builder(default)]
|
||||
pub pull: bool,
|
||||
|
||||
#[builder(default)]
|
||||
pub remove: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOptsVolume<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub path_or_vol_name: Cow<'scope, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub container_path: Cow<'scope, str>,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! run_volumes {
|
||||
($($host:expr => $container:expr),+ $(,)?) => {
|
||||
{
|
||||
[
|
||||
$($crate::drivers::opts::RunOptsVolume::builder()
|
||||
.path_or_vol_name($host)
|
||||
.container_path($container)
|
||||
.build(),)*
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct RunOptsEnv<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub key: Cow<'scope, str>,
|
||||
|
||||
#[builder(setter(into))]
|
||||
pub value: Cow<'scope, str>,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! run_envs {
|
||||
($($key:expr => $value:expr),+ $(,)?) => {
|
||||
{
|
||||
[
|
||||
$($crate::drivers::opts::RunOptsEnv::builder()
|
||||
.key($key)
|
||||
.value($value)
|
||||
.build(),)*
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
115
process/drivers/opts/signing.rs
Normal file
115
process/drivers/opts/signing.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
env, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use typed_builder::TypedBuilder;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
pub enum PrivateKey {
|
||||
Env(String),
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
pub trait PrivateKeyContents<T>
|
||||
where
|
||||
T: Zeroize,
|
||||
{
|
||||
/// Gets's the contents of the `PrivateKey`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the file or the environment couldn't be read.
|
||||
fn contents(&self) -> Result<Zeroizing<T>>;
|
||||
}
|
||||
|
||||
impl PrivateKeyContents<Vec<u8>> for PrivateKey {
|
||||
fn contents(&self) -> Result<Zeroizing<Vec<u8>>> {
|
||||
let key: Zeroizing<String> = self.contents()?;
|
||||
Ok(Zeroizing::new(key.as_bytes().to_vec()))
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivateKeyContents<String> for PrivateKey {
|
||||
fn contents(&self) -> Result<Zeroizing<String>> {
|
||||
Ok(Zeroizing::new(match *self {
|
||||
Self::Env(ref env) => env::var(env).into_diagnostic()?,
|
||||
Self::Path(ref path) => fs::read_to_string(path).into_diagnostic()?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PrivateKey {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(
|
||||
match *self {
|
||||
Self::Env(ref env) => format!("env://{env}"),
|
||||
Self::Path(ref path) => format!("{}", path.display()),
|
||||
}
|
||||
.as_str(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct GenerateKeyPairOpts<'scope> {
|
||||
#[builder(setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct CheckKeyPairOpts<'scope> {
|
||||
#[builder(setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct SignOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub key: Option<Cow<'scope, str>>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum VerifyType<'scope> {
|
||||
File(Cow<'scope, Path>),
|
||||
Keyless {
|
||||
issuer: Cow<'scope, str>,
|
||||
identity: Cow<'scope, str>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct VerifyOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
pub verify_type: VerifyType<'scope>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypedBuilder)]
|
||||
pub struct SignVerifyOpts<'scope> {
|
||||
#[builder(setter(into))]
|
||||
pub image: Cow<'scope, str>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub tag: Option<Cow<'scope, str>>,
|
||||
|
||||
#[builder(default, setter(into, strip_option))]
|
||||
pub dir: Option<Cow<'scope, Path>>,
|
||||
|
||||
/// Enable retry logic for pushing.
|
||||
#[builder(default)]
|
||||
pub retry_push: bool,
|
||||
|
||||
/// Number of times to retry pushing.
|
||||
///
|
||||
/// Defaults to 1.
|
||||
#[builder(default = 1)]
|
||||
pub retry_count: u8,
|
||||
}
|
||||
284
process/drivers/podman_driver.rs
Normal file
284
process/drivers/podman_driver.rs
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
use std::{
|
||||
io::Write,
|
||||
path::Path,
|
||||
process::{Command, ExitStatus, Stdio},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use blue_build_utils::{cmd, constants::SKOPEO_IMAGE, credentials::Credentials, string_vec};
|
||||
use colored::Colorize;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use miette::{bail, miette, IntoDiagnostic, Result};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::{
|
||||
drivers::image_metadata::ImageMetadata,
|
||||
logging::{CommandLogging, Logger},
|
||||
signal_handler::{add_cid, remove_cid, ContainerId, ContainerRuntime},
|
||||
};
|
||||
|
||||
use super::{
|
||||
opts::{BuildOpts, GetMetadataOpts, PushOpts, RunOpts, TagOpts},
|
||||
BuildDriver, DriverVersion, InspectDriver, RunDriver,
|
||||
};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PodmanVersionJsonClient {
|
||||
#[serde(alias = "Version")]
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct PodmanVersionJson {
|
||||
#[serde(alias = "Client")]
|
||||
pub client: PodmanVersionJsonClient,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PodmanDriver;
|
||||
|
||||
impl DriverVersion for PodmanDriver {
|
||||
// First podman version to use buildah v1.24
|
||||
// https://github.com/containers/podman/blob/main/RELEASE_NOTES.md#400
|
||||
const VERSION_REQ: &'static str = ">=4";
|
||||
|
||||
fn version() -> Result<Version> {
|
||||
trace!("PodmanDriver::version()");
|
||||
|
||||
trace!("podman version -f json");
|
||||
let output = cmd!("podman", "version", "-f", "json")
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
let version_json: PodmanVersionJson = serde_json::from_slice(&output.stdout)
|
||||
.inspect_err(|e| error!("{e}: {}", String::from_utf8_lossy(&output.stdout)))
|
||||
.into_diagnostic()?;
|
||||
trace!("{version_json:#?}");
|
||||
|
||||
Ok(version_json.client.version)
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildDriver for PodmanDriver {
|
||||
fn build(opts: &BuildOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::build({opts:#?})");
|
||||
|
||||
let command = cmd!(
|
||||
"podman",
|
||||
"build",
|
||||
"--pull=true",
|
||||
format!("--layers={}", !opts.squash),
|
||||
"-f",
|
||||
&*opts.containerfile,
|
||||
"-t",
|
||||
&*opts.image,
|
||||
".",
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Building Image")
|
||||
.into_diagnostic()?;
|
||||
|
||||
if status.success() {
|
||||
info!("Successfully built {}", opts.image);
|
||||
} else {
|
||||
bail!("Failed to build {}", opts.image);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tag(opts: &TagOpts) -> Result<()> {
|
||||
trace!("PodmanDriver::tag({opts:#?})");
|
||||
|
||||
let mut command = cmd!("podman", "tag", &*opts.src_image, &*opts.dest_image,);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command.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!("PodmanDriver::push({opts:#?})");
|
||||
|
||||
let command = cmd!(
|
||||
"podman",
|
||||
"push",
|
||||
format!(
|
||||
"--compression-format={}",
|
||||
opts.compression_type.unwrap_or_default()
|
||||
),
|
||||
&*opts.image,
|
||||
);
|
||||
|
||||
trace!("{command:?}");
|
||||
let status = command
|
||||
.status_image_ref_progress(&opts.image, "Pushing Image")
|
||||
.into_diagnostic()?;
|
||||
|
||||
if status.success() {
|
||||
info!("Successfully pushed {}!", opts.image);
|
||||
} else {
|
||||
bail!("Failed to push image {}", opts.image)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn login() -> Result<()> {
|
||||
trace!("PodmanDriver::login()");
|
||||
|
||||
if let Some(Credentials {
|
||||
registry,
|
||||
username,
|
||||
password,
|
||||
}) = Credentials::get()
|
||||
{
|
||||
let mut command = cmd!(
|
||||
"podman",
|
||||
"login",
|
||||
"-u",
|
||||
username,
|
||||
"--password-stdin",
|
||||
registry
|
||||
);
|
||||
command
|
||||
.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 podman:\n{}", err_out.trim());
|
||||
}
|
||||
debug!("Logged into {registry}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectDriver for PodmanDriver {
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("PodmanDriver::get_metadata({opts:#?})");
|
||||
|
||||
let url = opts.tag.as_deref().map_or_else(
|
||||
|| format!("docker://{}", opts.image),
|
||||
|tag| format!("docker://{}:{tag}", opts.image),
|
||||
);
|
||||
|
||||
let progress = Logger::multi_progress().add(
|
||||
ProgressBar::new_spinner()
|
||||
.with_style(ProgressStyle::default_spinner())
|
||||
.with_message(format!("Inspecting metadata for {url}")),
|
||||
);
|
||||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
let output = Self::run_output(
|
||||
&RunOpts::builder()
|
||||
.image(SKOPEO_IMAGE)
|
||||
.args(string_vec!["inspect", url.clone()])
|
||||
.remove(true)
|
||||
.build(),
|
||||
)
|
||||
.into_diagnostic()?;
|
||||
|
||||
progress.finish();
|
||||
Logger::multi_progress().remove(&progress);
|
||||
|
||||
if output.status.success() {
|
||||
debug!("Successfully inspected image {url}!");
|
||||
} else {
|
||||
bail!("Failed to inspect image {url}");
|
||||
}
|
||||
serde_json::from_slice(&output.stdout).into_diagnostic()
|
||||
}
|
||||
}
|
||||
|
||||
impl RunDriver for PodmanDriver {
|
||||
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus> {
|
||||
trace!("PodmanDriver::run({opts:#?})");
|
||||
|
||||
let cid_path = TempDir::new("podman")?;
|
||||
let cid_file = cid_path.path().join("cid");
|
||||
|
||||
let cid = ContainerId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
|
||||
|
||||
add_cid(&cid);
|
||||
|
||||
let status = podman_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!("PodmanDriver::run_output({opts:#?})");
|
||||
|
||||
let cid_path = TempDir::new("podman")?;
|
||||
let cid_file = cid_path.path().join("cid");
|
||||
|
||||
let cid = ContainerId::new(&cid_file, ContainerRuntime::Podman, opts.privileged);
|
||||
|
||||
add_cid(&cid);
|
||||
|
||||
let output = podman_run(opts, &cid_file).output()?;
|
||||
|
||||
remove_cid(&cid);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
fn podman_run(opts: &RunOpts, cid_file: &Path) -> Command {
|
||||
cmd!(
|
||||
if opts.privileged {
|
||||
warn!(
|
||||
"Running 'podman' in privileged mode requires '{}'",
|
||||
"sudo".bold().red()
|
||||
);
|
||||
"sudo"
|
||||
} else {
|
||||
"podman"
|
||||
},
|
||||
if opts.privileged => "podman",
|
||||
"run",
|
||||
format!("--cidfile={}", cid_file.display()),
|
||||
if opts.privileged => "--privileged",
|
||||
if opts.remove => "--rm",
|
||||
if opts.pull => "--pull=always",
|
||||
for volume in opts.volumes => [
|
||||
"--volume",
|
||||
format!("{}:{}", volume.path_or_vol_name, volume.container_path),
|
||||
],
|
||||
for env in opts.env_vars => [
|
||||
"--env",
|
||||
format!("{}={}", env.key, env.value),
|
||||
],
|
||||
&*opts.image,
|
||||
for opts.args,
|
||||
)
|
||||
}
|
||||
288
process/drivers/sigstore_driver.rs
Normal file
288
process/drivers/sigstore_driver.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
use std::{fs, path::Path};
|
||||
|
||||
use crate::{
|
||||
drivers::opts::{PrivateKeyContents, VerifyType},
|
||||
RT,
|
||||
};
|
||||
|
||||
use super::{
|
||||
functions::get_private_key,
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
use blue_build_utils::{
|
||||
constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH},
|
||||
credentials::Credentials,
|
||||
};
|
||||
use log::{debug, trace};
|
||||
use miette::{bail, miette, Context, IntoDiagnostic};
|
||||
use sigstore::{
|
||||
cosign::{
|
||||
constraint::PrivateKeySigner,
|
||||
verification_constraint::{PublicKeyVerifier, VerificationConstraintVec},
|
||||
ClientBuilder, Constraint, CosignCapabilities, SignatureLayer,
|
||||
},
|
||||
crypto::{signing_key::SigStoreKeyPair, SigningScheme},
|
||||
errors::SigstoreVerifyConstraintsError,
|
||||
registry::{Auth, OciReference},
|
||||
};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub struct SigstoreDriver;
|
||||
|
||||
impl SigningDriver for SigstoreDriver {
|
||||
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> miette::Result<()> {
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let priv_key_path = path.join(COSIGN_PRIV_PATH);
|
||||
let pub_key_path = path.join(COSIGN_PUB_PATH);
|
||||
|
||||
if priv_key_path.exists() {
|
||||
bail!("Private key file already exists at {COSIGN_PRIV_PATH}");
|
||||
} else if pub_key_path.exists() {
|
||||
bail!("Public key file already exists at {COSIGN_PUB_PATH}");
|
||||
}
|
||||
|
||||
let signer = SigningScheme::default()
|
||||
.create_signer()
|
||||
.into_diagnostic()
|
||||
.context("Failed to create signer")?;
|
||||
|
||||
let keypair = signer
|
||||
.to_sigstore_keypair()
|
||||
.into_diagnostic()
|
||||
.context("Failed to create key pair")?;
|
||||
|
||||
let priv_key = keypair
|
||||
.private_key_to_encrypted_pem(b"")
|
||||
.into_diagnostic()
|
||||
.context("Failed to create encrypted private key")?;
|
||||
let pub_key = keypair.public_key_to_pem().into_diagnostic()?;
|
||||
|
||||
fs::write(priv_key_path, priv_key)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to write {COSIGN_PRIV_PATH}"))?;
|
||||
fs::write(pub_key_path, pub_key)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to write {COSIGN_PUB_PATH}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_signing_files(opts: &CheckKeyPairOpts) -> miette::Result<()> {
|
||||
trace!("SigstoreDriver::check_signing_files({opts:?})");
|
||||
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let pub_path = path.join(COSIGN_PUB_PATH);
|
||||
|
||||
let pub_key = fs::read_to_string(&pub_path)
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to open public key file {}", pub_path.display()))?;
|
||||
debug!("Retrieved public key from {COSIGN_PUB_PATH}");
|
||||
trace!("{pub_key}");
|
||||
|
||||
let key: Zeroizing<String> = get_private_key(path)
|
||||
.context("Failed to get private key")?
|
||||
.contents()?;
|
||||
debug!("Retrieved private key");
|
||||
|
||||
let keypair = SigStoreKeyPair::from_encrypted_pem(key.as_bytes(), b"")
|
||||
.into_diagnostic()
|
||||
.context("Failed to generate key pair from private key")?;
|
||||
let gen_pub = keypair
|
||||
.public_key_to_pem()
|
||||
.into_diagnostic()
|
||||
.context("Failed to generate public key from private key")?;
|
||||
debug!("Generated public key from private key");
|
||||
trace!("{gen_pub}");
|
||||
|
||||
if pub_key.trim() == gen_pub.trim() {
|
||||
debug!("Public and private key matches");
|
||||
Ok(())
|
||||
} else {
|
||||
bail!("Private and public keys do not match.")
|
||||
}
|
||||
}
|
||||
|
||||
fn sign(opts: &SignOpts) -> miette::Result<()> {
|
||||
trace!("SigstoreDriver::sign({opts:?})");
|
||||
|
||||
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
|
||||
let mut client = ClientBuilder::default().build().into_diagnostic()?;
|
||||
|
||||
let image_digest: &str = opts.image.as_ref();
|
||||
let image_digest: OciReference = image_digest.parse().into_diagnostic()?;
|
||||
trace!("{image_digest:?}");
|
||||
|
||||
let signing_scheme = SigningScheme::default();
|
||||
let key: Zeroizing<Vec<u8>> = get_private_key(path)?.contents()?;
|
||||
debug!("Retrieved private key");
|
||||
|
||||
let signer = PrivateKeySigner::new_with_signer(
|
||||
SigStoreKeyPair::from_encrypted_pem(&key, b"")
|
||||
.into_diagnostic()?
|
||||
.to_sigstore_signer(&signing_scheme)
|
||||
.into_diagnostic()?,
|
||||
);
|
||||
debug!("Created signer");
|
||||
|
||||
let Credentials {
|
||||
registry: _,
|
||||
username,
|
||||
password,
|
||||
} = Credentials::get().ok_or_else(|| miette!("Credentials are required for signing"))?;
|
||||
let auth = Auth::Basic(username.clone(), password.clone());
|
||||
debug!("Credentials retrieved");
|
||||
|
||||
let (cosign_signature_image, source_image_digest) = RT
|
||||
.block_on(client.triangulate(&image_digest, &auth))
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to triangulate image {image_digest}"))?;
|
||||
debug!("Triangulating image");
|
||||
trace!("{cosign_signature_image}, {source_image_digest}");
|
||||
|
||||
let mut signature_layer =
|
||||
SignatureLayer::new_unsigned(&image_digest, &source_image_digest).into_diagnostic()?;
|
||||
signer
|
||||
.add_constraint(&mut signature_layer)
|
||||
.into_diagnostic()?;
|
||||
debug!("Created signing layer");
|
||||
|
||||
debug!("Pushing signature");
|
||||
RT.block_on(client.push_signature(
|
||||
None,
|
||||
&auth,
|
||||
&cosign_signature_image,
|
||||
vec![signature_layer],
|
||||
))
|
||||
.into_diagnostic()
|
||||
.with_context(|| {
|
||||
format!("Failed to push signature {cosign_signature_image} for image {image_digest}")
|
||||
})?;
|
||||
debug!("Successfully pushed signature");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify(opts: &VerifyOpts) -> miette::Result<()> {
|
||||
let mut client = ClientBuilder::default().build().into_diagnostic()?;
|
||||
|
||||
let image_digest: &str = opts.image.as_ref();
|
||||
let image_digest: OciReference = image_digest.parse().into_diagnostic()?;
|
||||
trace!("{image_digest:?}");
|
||||
|
||||
let signing_scheme = SigningScheme::default();
|
||||
|
||||
let pub_key = fs::read_to_string(match &opts.verify_type {
|
||||
VerifyType::File(path) => path,
|
||||
VerifyType::Keyless { .. } => {
|
||||
todo!("Keyless currently not supported for sigstore driver")
|
||||
}
|
||||
})
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to open public key file {COSIGN_PUB_PATH}"))?;
|
||||
debug!("Retrieved public key from {COSIGN_PUB_PATH}");
|
||||
trace!("{pub_key}");
|
||||
|
||||
let verifier =
|
||||
PublicKeyVerifier::new(pub_key.as_bytes(), &signing_scheme).into_diagnostic()?;
|
||||
let verification_constraints: VerificationConstraintVec = vec![Box::new(verifier)];
|
||||
|
||||
let auth = Auth::Anonymous;
|
||||
let (cosign_signature_image, source_image_digest) = RT
|
||||
.block_on(client.triangulate(&image_digest, &auth))
|
||||
.into_diagnostic()
|
||||
.with_context(|| format!("Failed to triangulate image {image_digest}"))?;
|
||||
debug!("Triangulating image");
|
||||
trace!("{cosign_signature_image}, {source_image_digest}");
|
||||
|
||||
let trusted_layers = RT
|
||||
.block_on(client.trusted_signature_layers(
|
||||
&auth,
|
||||
&source_image_digest,
|
||||
&cosign_signature_image,
|
||||
))
|
||||
.into_diagnostic()?;
|
||||
|
||||
sigstore::cosign::verify_constraints(&trusted_layers, verification_constraints.iter())
|
||||
.map_err(
|
||||
|SigstoreVerifyConstraintsError {
|
||||
unsatisfied_constraints,
|
||||
}| {
|
||||
miette!("Failed to verify for constraints: {unsatisfied_constraints:?}")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn signing_login() -> miette::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{fs, path::Path};
|
||||
|
||||
use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
|
||||
use tempdir::TempDir;
|
||||
|
||||
use crate::drivers::{
|
||||
cosign_driver::CosignDriver,
|
||||
opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
|
||||
SigningDriver,
|
||||
};
|
||||
|
||||
use super::SigstoreDriver;
|
||||
|
||||
#[test]
|
||||
fn generate_key_pair() {
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_key_pairs() {
|
||||
let path = Path::new("../test-files/keys");
|
||||
|
||||
let opts = CheckKeyPairOpts::builder().dir(path).build();
|
||||
|
||||
SigstoreDriver::check_signing_files(&opts).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compatibility() {
|
||||
let tempdir = TempDir::new("keypair").unwrap();
|
||||
|
||||
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
SigstoreDriver::generate_key_pair(&gen_opts).unwrap();
|
||||
|
||||
eprintln!(
|
||||
"Private key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
|
||||
);
|
||||
eprintln!(
|
||||
"Public key:\n{}",
|
||||
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
|
||||
);
|
||||
|
||||
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
|
||||
|
||||
CosignDriver::check_signing_files(&check_opts).unwrap();
|
||||
}
|
||||
}
|
||||
47
process/drivers/skopeo_driver.rs
Normal file
47
process/drivers/skopeo_driver.rs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
use std::{process::Stdio, time::Duration};
|
||||
|
||||
use blue_build_utils::cmd;
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
use log::{debug, trace};
|
||||
use miette::{bail, IntoDiagnostic, Result};
|
||||
|
||||
use crate::logging::Logger;
|
||||
|
||||
use super::{image_metadata::ImageMetadata, opts::GetMetadataOpts, InspectDriver};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SkopeoDriver;
|
||||
|
||||
impl InspectDriver for SkopeoDriver {
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata> {
|
||||
trace!("SkopeoDriver::get_metadata({opts:#?})");
|
||||
|
||||
let url = opts.tag.as_ref().map_or_else(
|
||||
|| format!("docker://{}", opts.image),
|
||||
|tag| format!("docker://{}:{tag}", opts.image),
|
||||
);
|
||||
|
||||
let progress = Logger::multi_progress().add(
|
||||
ProgressBar::new_spinner()
|
||||
.with_style(ProgressStyle::default_spinner())
|
||||
.with_message(format!("Inspecting metadata for {url}")),
|
||||
);
|
||||
progress.enable_steady_tick(Duration::from_millis(100));
|
||||
|
||||
trace!("skopeo inspect {url}");
|
||||
let output = cmd!("skopeo", "inspect", &url)
|
||||
.stderr(Stdio::inherit())
|
||||
.output()
|
||||
.into_diagnostic()?;
|
||||
|
||||
progress.finish();
|
||||
Logger::multi_progress().remove(&progress);
|
||||
|
||||
if output.status.success() {
|
||||
debug!("Successfully inspected image {url}!");
|
||||
} else {
|
||||
bail!("Failed to inspect image {url}")
|
||||
}
|
||||
serde_json::from_slice(&output.stdout).into_diagnostic()
|
||||
}
|
||||
}
|
||||
335
process/drivers/traits.rs
Normal file
335
process/drivers/traits.rs
Normal file
|
|
@ -0,0 +1,335 @@
|
|||
use std::{
|
||||
path::PathBuf,
|
||||
process::{ExitStatus, Output},
|
||||
};
|
||||
|
||||
use blue_build_recipe::Recipe;
|
||||
use blue_build_utils::{constants::COSIGN_PUB_PATH, retry};
|
||||
use log::{debug, info, trace};
|
||||
use miette::{bail, miette, Result};
|
||||
use semver::{Version, VersionReq};
|
||||
|
||||
use crate::drivers::{functions::get_private_key, types::CiDriverType, Driver};
|
||||
|
||||
use super::{
|
||||
image_metadata::ImageMetadata,
|
||||
opts::{
|
||||
BuildOpts, BuildTagPushOpts, CheckKeyPairOpts, GenerateKeyPairOpts, GetMetadataOpts,
|
||||
PushOpts, RunOpts, SignOpts, SignVerifyOpts, TagOpts, VerifyOpts, VerifyType,
|
||||
},
|
||||
};
|
||||
|
||||
/// Trait for retrieving version of a driver.
|
||||
pub trait DriverVersion {
|
||||
/// The version req string slice that follows
|
||||
/// the semver standard <https://semver.org/>.
|
||||
const VERSION_REQ: &'static str;
|
||||
|
||||
/// Returns the version of the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it can't retrieve the version.
|
||||
fn version() -> Result<Version>;
|
||||
|
||||
#[must_use]
|
||||
fn is_supported_version() -> bool {
|
||||
Self::version().is_ok_and(|version| {
|
||||
VersionReq::parse(Self::VERSION_REQ).is_ok_and(|req| req.matches(&version))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows agnostic building, tagging
|
||||
/// pushing, and login.
|
||||
pub trait BuildDriver {
|
||||
/// Runs the build logic for the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the build fails.
|
||||
fn build(opts: &BuildOpts) -> Result<()>;
|
||||
|
||||
/// Runs the tag logic for the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the tagging fails.
|
||||
fn tag(opts: &TagOpts) -> Result<()>;
|
||||
|
||||
/// Runs the push logic for the driver
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the push fails.
|
||||
fn push(opts: &PushOpts) -> Result<()>;
|
||||
|
||||
/// Runs the login logic for the driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if login fails.
|
||||
fn login() -> Result<()>;
|
||||
|
||||
/// Runs the logic for building, tagging, and pushing an image.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if building, tagging, or pusing fails.
|
||||
fn build_tag_push(opts: &BuildTagPushOpts) -> Result<()> {
|
||||
trace!("BuildDriver::build_tag_push({opts:#?})");
|
||||
|
||||
let full_image = match (opts.archive_path.as_ref(), opts.image.as_ref()) {
|
||||
(Some(archive_path), None) => {
|
||||
format!("oci-archive:{archive_path}")
|
||||
}
|
||||
(None, Some(image)) => opts
|
||||
.tags
|
||||
.first()
|
||||
.map_or_else(|| image.to_string(), |tag| format!("{image}:{tag}")),
|
||||
(Some(_), Some(_)) => bail!("Cannot use both image and archive path"),
|
||||
(None, None) => bail!("Need either the image or archive path set"),
|
||||
};
|
||||
|
||||
let build_opts = BuildOpts::builder()
|
||||
.image(&full_image)
|
||||
.containerfile(opts.containerfile.as_ref())
|
||||
.squash(opts.squash)
|
||||
.build();
|
||||
|
||||
info!("Building image {full_image}");
|
||||
Self::build(&build_opts)?;
|
||||
|
||||
if !opts.tags.is_empty() && opts.archive_path.is_none() {
|
||||
let image = opts
|
||||
.image
|
||||
.as_ref()
|
||||
.ok_or_else(|| miette!("Image is required in order to tag"))?;
|
||||
debug!("Tagging all images");
|
||||
|
||||
for tag in opts.tags.as_ref() {
|
||||
debug!("Tagging {} with {tag}", &full_image);
|
||||
|
||||
let tag_opts = TagOpts::builder()
|
||||
.src_image(&full_image)
|
||||
.dest_image(format!("{image}:{tag}"))
|
||||
.build();
|
||||
|
||||
Self::tag(&tag_opts)?;
|
||||
|
||||
if opts.push {
|
||||
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
|
||||
|
||||
debug!("Pushing all images");
|
||||
// Push images with retries (1s delay between retries)
|
||||
blue_build_utils::retry(retry_count, 5, || {
|
||||
let tag_image = format!("{image}:{tag}");
|
||||
|
||||
debug!("Pushing image {tag_image}");
|
||||
|
||||
let push_opts = PushOpts::builder()
|
||||
.image(&tag_image)
|
||||
.compression_type(opts.compression)
|
||||
.build();
|
||||
|
||||
Self::push(&push_opts)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows agnostic inspection of images.
|
||||
pub trait InspectDriver {
|
||||
/// Gets the metadata on an image tag.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it is unable to get the labels.
|
||||
fn get_metadata(opts: &GetMetadataOpts) -> Result<ImageMetadata>;
|
||||
}
|
||||
|
||||
pub trait RunDriver: Sync + Send {
|
||||
/// Run a container to perform an action.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if there is an issue running the container.
|
||||
fn run(opts: &RunOpts) -> std::io::Result<ExitStatus>;
|
||||
|
||||
/// Run a container to perform an action and capturing output.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if there is an issue running the container.
|
||||
fn run_output(opts: &RunOpts) -> std::io::Result<Output>;
|
||||
}
|
||||
|
||||
pub trait SigningDriver {
|
||||
/// Generate a new private/public key pair.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if a key-pair couldn't be generated.
|
||||
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()>;
|
||||
|
||||
/// Checks the signing key files to ensure
|
||||
/// they match.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the files cannot be verified.
|
||||
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()>;
|
||||
|
||||
/// Signs the image digest.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if signing fails.
|
||||
fn sign(opts: &SignOpts) -> Result<()>;
|
||||
|
||||
/// Verifies the image.
|
||||
///
|
||||
/// The image can be verified either with `VerifyType::File` containing
|
||||
/// the public key contents, or with `VerifyType::Keyless` containing
|
||||
/// information about the `issuer` and `identity`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the image fails to be verified.
|
||||
fn verify(opts: &VerifyOpts) -> Result<()>;
|
||||
|
||||
/// Sign an image given the image name and tag.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the image fails to be signed.
|
||||
fn sign_and_verify(opts: &SignVerifyOpts) -> Result<()> {
|
||||
trace!("sign_and_verify({opts:?})");
|
||||
|
||||
let path = opts
|
||||
.dir
|
||||
.as_ref()
|
||||
.map_or_else(|| PathBuf::from("."), |d| d.to_path_buf());
|
||||
|
||||
let image_name: &str = opts.image.as_ref();
|
||||
let inspect_opts = GetMetadataOpts::builder().image(image_name);
|
||||
|
||||
let inspect_opts = if let Some(ref tag) = opts.tag {
|
||||
inspect_opts.tag(tag.as_ref() as &str).build()
|
||||
} else {
|
||||
inspect_opts.build()
|
||||
};
|
||||
|
||||
let image_digest = Driver::get_metadata(&inspect_opts)?.digest;
|
||||
let image_name_tag = opts
|
||||
.tag
|
||||
.as_ref()
|
||||
.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}"));
|
||||
let image_digest = format!("{image_name}@{image_digest}");
|
||||
|
||||
let (sign_opts, verify_opts) = match (Driver::get_ci_driver(), get_private_key(&path)) {
|
||||
// Cosign public/private key pair
|
||||
(_, Ok(priv_key)) => (
|
||||
SignOpts::builder()
|
||||
.image(&image_digest)
|
||||
.dir(&path)
|
||||
.key(priv_key.to_string())
|
||||
.build(),
|
||||
VerifyOpts::builder()
|
||||
.image(&image_name_tag)
|
||||
.verify_type(VerifyType::File(path.join(COSIGN_PUB_PATH).into()))
|
||||
.build(),
|
||||
),
|
||||
// Gitlab keyless
|
||||
(CiDriverType::Github | CiDriverType::Gitlab, _) => (
|
||||
SignOpts::builder().dir(&path).image(&image_digest).build(),
|
||||
VerifyOpts::builder()
|
||||
.image(&image_name_tag)
|
||||
.verify_type(VerifyType::Keyless {
|
||||
issuer: Driver::oidc_provider()?.into(),
|
||||
identity: Driver::keyless_cert_identity()?.into(),
|
||||
})
|
||||
.build(),
|
||||
),
|
||||
_ => bail!("Failed to get information for signing the image"),
|
||||
};
|
||||
|
||||
let retry_count = if opts.retry_push { opts.retry_count } else { 0 };
|
||||
|
||||
retry(retry_count, 5, || {
|
||||
Self::sign(&sign_opts)?;
|
||||
Self::verify(&verify_opts)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs the login logic for the signing driver.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if login fails.
|
||||
fn signing_login() -> Result<()>;
|
||||
}
|
||||
|
||||
/// Allows agnostic retrieval of CI-based information.
|
||||
pub trait CiDriver {
|
||||
/// Determines if we're on the main branch of
|
||||
/// a repository.
|
||||
fn on_default_branch() -> bool;
|
||||
|
||||
/// Retrieve the certificate identity for
|
||||
/// keyless signing.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn keyless_cert_identity() -> Result<String>;
|
||||
|
||||
/// Retrieve the OIDC Provider for keyless signing.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn oidc_provider() -> Result<String>;
|
||||
|
||||
/// Generate a list of tags based on the OS version.
|
||||
///
|
||||
/// ## CI
|
||||
/// The tags are generated based on the CI system that
|
||||
/// is detected. The general format for the default branch is:
|
||||
/// - `${os_version}`
|
||||
/// - `${timestamp}-${os_version}`
|
||||
///
|
||||
/// On a branch:
|
||||
/// - `br-${branch_name}-${os_version}`
|
||||
///
|
||||
/// In a PR(GitHub)/MR(GitLab)
|
||||
/// - `pr-${pr_event_number}-${os_version}`/`mr-${mr_iid}-${os_version}`
|
||||
///
|
||||
/// In all above cases the short git sha is also added:
|
||||
/// - `${commit_sha}-${os_version}`
|
||||
///
|
||||
/// When `alt_tags` are not present, the following tags are added:
|
||||
/// - `latest`
|
||||
/// - `${timestamp}`
|
||||
///
|
||||
/// ## Locally
|
||||
/// When ran locally, only a local tag is created:
|
||||
/// - `local-${os_version}`
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn generate_tags(recipe: &Recipe) -> Result<Vec<String>>;
|
||||
|
||||
/// Generates the image name based on CI.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn generate_image_name(recipe: &Recipe) -> Result<String> {
|
||||
Ok(format!(
|
||||
"{}/{}",
|
||||
Self::get_registry()?,
|
||||
recipe.name.trim().to_lowercase()
|
||||
))
|
||||
}
|
||||
|
||||
/// Get the URL for the repository.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn get_repo_url() -> Result<String>;
|
||||
|
||||
/// Get the registry ref for the image.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if the environment variables aren't set.
|
||||
fn get_registry() -> Result<String>;
|
||||
}
|
||||
169
process/drivers/types.rs
Normal file
169
process/drivers/types.rs
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
use std::env;
|
||||
|
||||
use blue_build_utils::constants::{GITHUB_ACTIONS, GITLAB_CI};
|
||||
use clap::ValueEnum;
|
||||
use log::trace;
|
||||
|
||||
use crate::drivers::{
|
||||
buildah_driver::BuildahDriver, docker_driver::DockerDriver, podman_driver::PodmanDriver,
|
||||
DriverVersion,
|
||||
};
|
||||
|
||||
pub(super) trait DetermineDriver<T> {
|
||||
fn determine_driver(&mut self) -> T;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum InspectDriverType {
|
||||
Skopeo,
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl DetermineDriver<InspectDriverType> for Option<InspectDriverType> {
|
||||
fn determine_driver(&mut self) -> InspectDriverType {
|
||||
*self.get_or_insert(
|
||||
match (
|
||||
blue_build_utils::check_command_exists("skopeo"),
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(_skopeo), _, _) => InspectDriverType::Skopeo,
|
||||
(_, Ok(_docker), _) => InspectDriverType::Docker,
|
||||
(_, _, Ok(_podman)) => InspectDriverType::Podman,
|
||||
_ => panic!(
|
||||
"{}{}",
|
||||
"Could not determine inspection strategy. ",
|
||||
"You need either skopeo, docker, or podman",
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum BuildDriverType {
|
||||
Buildah,
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl DetermineDriver<BuildDriverType> for Option<BuildDriverType> {
|
||||
fn determine_driver(&mut self) -> BuildDriverType {
|
||||
*self.get_or_insert(
|
||||
match (
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
blue_build_utils::check_command_exists("buildah"),
|
||||
) {
|
||||
(Ok(_docker), _, _) if DockerDriver::is_supported_version() => {
|
||||
BuildDriverType::Docker
|
||||
}
|
||||
(_, Ok(_podman), _) if PodmanDriver::is_supported_version() => {
|
||||
BuildDriverType::Podman
|
||||
}
|
||||
(_, _, Ok(_buildah)) if BuildahDriver::is_supported_version() => {
|
||||
BuildDriverType::Buildah
|
||||
}
|
||||
_ => panic!(
|
||||
"{}{}{}{}",
|
||||
"Could not determine strategy, ",
|
||||
format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ,),
|
||||
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ,),
|
||||
format_args!(
|
||||
"or buildah version {} to continue",
|
||||
BuildahDriver::VERSION_REQ,
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum SigningDriverType {
|
||||
Cosign,
|
||||
#[cfg(feature = "sigstore")]
|
||||
Sigstore,
|
||||
}
|
||||
|
||||
impl DetermineDriver<SigningDriverType> for Option<SigningDriverType> {
|
||||
fn determine_driver(&mut self) -> SigningDriverType {
|
||||
trace!("SigningDriverType::determine_signing_driver()");
|
||||
|
||||
#[cfg(feature = "sigstore")]
|
||||
{
|
||||
*self.get_or_insert(
|
||||
blue_build_utils::check_command_exists("cosign")
|
||||
.map_or(SigningDriverType::Sigstore, |()| SigningDriverType::Cosign),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sigstore"))]
|
||||
{
|
||||
SigningDriverType::Cosign
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum RunDriverType {
|
||||
Podman,
|
||||
Docker,
|
||||
}
|
||||
|
||||
impl From<RunDriverType> for String {
|
||||
fn from(value: RunDriverType) -> Self {
|
||||
match value {
|
||||
RunDriverType::Podman => "podman".to_string(),
|
||||
RunDriverType::Docker => "docker".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DetermineDriver<RunDriverType> for Option<RunDriverType> {
|
||||
fn determine_driver(&mut self) -> RunDriverType {
|
||||
trace!("RunDriver::determine_driver()");
|
||||
|
||||
*self.get_or_insert(
|
||||
match (
|
||||
blue_build_utils::check_command_exists("docker"),
|
||||
blue_build_utils::check_command_exists("podman"),
|
||||
) {
|
||||
(Ok(_docker), _) if DockerDriver::is_supported_version() => RunDriverType::Docker,
|
||||
(_, Ok(_podman)) if PodmanDriver::is_supported_version() => RunDriverType::Podman,
|
||||
_ => panic!(
|
||||
"{}{}{}{}",
|
||||
"Could not determine strategy, ",
|
||||
format_args!("need either docker version {}, ", DockerDriver::VERSION_REQ),
|
||||
format_args!("podman version {}, ", PodmanDriver::VERSION_REQ),
|
||||
format_args!(
|
||||
"or buildah version {} to continue",
|
||||
BuildahDriver::VERSION_REQ
|
||||
),
|
||||
),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, ValueEnum)]
|
||||
pub enum CiDriverType {
|
||||
Local,
|
||||
Gitlab,
|
||||
Github,
|
||||
}
|
||||
|
||||
impl DetermineDriver<CiDriverType> for Option<CiDriverType> {
|
||||
fn determine_driver(&mut self) -> CiDriverType {
|
||||
trace!("CiDriverType::determine_driver()");
|
||||
|
||||
*self.get_or_insert(
|
||||
match (env::var(GITLAB_CI).ok(), env::var(GITHUB_ACTIONS).ok()) {
|
||||
(Some(_gitlab_ci), None) => CiDriverType::Gitlab,
|
||||
(None, Some(_github_actions)) => CiDriverType::Github,
|
||||
_ => CiDriverType::Local,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue