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:
Gerald Pinder 2024-08-12 23:52:07 -04:00 committed by GitHub
parent 3ecb0d3d93
commit 8ce83ba7ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 6468 additions and 2083 deletions

View 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(())
}
}

View 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();
}
}

View 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,
)
}

View 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",
)
}
},
)
}

View 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();
}
}

View 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,
}

View 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();
}
}

View 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,
)
}
}

View 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
View 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",
})
}
}

View 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,
}

View 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>>,
}

View 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(),)*
]
}
};
}

View 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,
}

View 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,
)
}

View 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();
}
}

View 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
View 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
View 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,
},
)
}
}