fix: Allow docker driver to properly use cache (#126)
This fix involves creating a new function for the `BuildDriver` trait called `build_tag_push`. In order to get the proper logic in place to make use of `docker buildx`, I had to create a separate function that would construct the build command to include all of the tags necessary for pushing. A default implementation of `build_tag_push` will be used for `podman` and `buildah` which was originally from the build command's functions. Now that we have custom logic for docker builds, we can take advantage of using the GitHub cache features without having the `--load` arg which had a big negative effect on build times. We can now also use docker for creating local `oci-archive` tarballs for local rebasing. Making use of the `oci-archive` will require the user to create a `docker-container` builder as it is not supported on the standard `docker` builder. https://docs.docker.com/build/exporters/oci-docker/
This commit is contained in:
parent
5fc4096f0f
commit
7c34d0c5a8
11 changed files with 304 additions and 193 deletions
2
.github/workflows/build-pr.yml
vendored
2
.github/workflows/build-pr.yml
vendored
|
|
@ -91,7 +91,6 @@ jobs:
|
|||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
driver: docker
|
||||
install: true
|
||||
|
||||
- name: Earthly login
|
||||
|
|
@ -119,6 +118,7 @@ jobs:
|
|||
GH_TOKEN: ${{ github.token }}
|
||||
GH_PR_EVENT_NUMBER: ${{ github.event.number }}
|
||||
COSIGN_PRIVATE_KEY: ${{ secrets.TEST_SIGNING_SECRET }}
|
||||
BB_BUILDKIT_CACHE_GHA: true
|
||||
run: |
|
||||
cd integration-tests/test-repo
|
||||
if [ -n "$GH_TOKEN" ] && [ -n "$COSIGN_PRIVATE_KEY" ]; then
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ use colorized::{Color, Colors};
|
|||
use log::{debug, info, trace, warn};
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
use crate::{commands::template::TemplateCommand, credentials, drivers::Driver};
|
||||
use crate::{
|
||||
commands::template::TemplateCommand,
|
||||
credentials,
|
||||
drivers::{opts::BuildTagPushOpts, Driver},
|
||||
};
|
||||
|
||||
use super::BlueBuildCommand;
|
||||
|
||||
|
|
@ -215,7 +219,6 @@ impl BlueBuildCommand for BuildCommand {
|
|||
|
||||
if self.push {
|
||||
blue_build_utils::check_command_exists("cosign")?;
|
||||
blue_build_utils::check_command_exists("skopeo")?;
|
||||
check_cosign_files()?;
|
||||
}
|
||||
|
||||
|
|
@ -238,7 +241,29 @@ impl BuildCommand {
|
|||
Self::login()?;
|
||||
}
|
||||
|
||||
self.run_build(&image_name, &tags)?;
|
||||
let opts = if let Some(archive_dir) = self.archive.as_ref() {
|
||||
BuildTagPushOpts::builder()
|
||||
.archive_path(format!(
|
||||
"{}/{}.{ARCHIVE_SUFFIX}",
|
||||
archive_dir.to_string_lossy().trim_end_matches('/'),
|
||||
recipe.name.to_lowercase().replace('/', "_"),
|
||||
))
|
||||
.build()
|
||||
} else {
|
||||
BuildTagPushOpts::builder()
|
||||
.image(&image_name)
|
||||
.tags(tags.iter().map(String::as_str).collect::<Vec<_>>())
|
||||
.push(self.push)
|
||||
.no_retry_push(self.no_retry_push)
|
||||
.retry_count(self.retry_count)
|
||||
.build()
|
||||
};
|
||||
|
||||
Driver::get_build_driver().build_tag_push(&opts)?;
|
||||
|
||||
if self.push {
|
||||
sign_images(&image_name, tags.first().map(String::as_str))?;
|
||||
}
|
||||
|
||||
info!("Build complete!");
|
||||
|
||||
|
|
@ -286,61 +311,53 @@ impl BuildCommand {
|
|||
trace!("BuildCommand::generate_full_image_name({recipe:#?})");
|
||||
info!("Generating full image name");
|
||||
|
||||
let image_name = if let Some(archive_dir) = &self.archive {
|
||||
format!(
|
||||
"oci-archive:{}/{}.{ARCHIVE_SUFFIX}",
|
||||
archive_dir.to_string_lossy().trim_end_matches('/'),
|
||||
recipe.name.to_lowercase().replace('/', "_"),
|
||||
)
|
||||
} else {
|
||||
match (
|
||||
env::var(CI_REGISTRY).ok().map(|s| s.to_lowercase()),
|
||||
env::var(CI_PROJECT_NAMESPACE)
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase()),
|
||||
env::var(CI_PROJECT_NAME).ok().map(|s| s.to_lowercase()),
|
||||
env::var(GITHUB_REPOSITORY_OWNER)
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase()),
|
||||
self.registry.as_ref().map(|s| s.to_lowercase()),
|
||||
self.registry_namespace.as_ref().map(|s| s.to_lowercase()),
|
||||
) {
|
||||
(_, _, _, _, Some(registry), Some(registry_path)) => {
|
||||
trace!("registry={registry}, registry_path={registry_path}");
|
||||
format!(
|
||||
"{}/{}/{}",
|
||||
registry.trim().trim_matches('/'),
|
||||
registry_path.trim().trim_matches('/'),
|
||||
recipe.name.trim(),
|
||||
)
|
||||
}
|
||||
(
|
||||
Some(ci_registry),
|
||||
Some(ci_project_namespace),
|
||||
Some(ci_project_name),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) => {
|
||||
trace!("CI_REGISTRY={ci_registry}, CI_PROJECT_NAMESPACE={ci_project_namespace}, CI_PROJECT_NAME={ci_project_name}");
|
||||
warn!("Generating Gitlab Registry image");
|
||||
format!(
|
||||
"{ci_registry}/{ci_project_namespace}/{ci_project_name}/{}",
|
||||
recipe.name.trim().to_lowercase()
|
||||
)
|
||||
}
|
||||
(None, None, None, Some(github_repository_owner), None, None) => {
|
||||
trace!("GITHUB_REPOSITORY_OWNER={github_repository_owner}");
|
||||
warn!("Generating Github Registry image");
|
||||
format!("ghcr.io/{github_repository_owner}/{}", &recipe.name)
|
||||
}
|
||||
_ => {
|
||||
trace!("Nothing to indicate an image name with a registry");
|
||||
if self.push {
|
||||
bail!("Need '--registry' and '--registry-path' in order to push image");
|
||||
}
|
||||
let image_name = match (
|
||||
env::var(CI_REGISTRY).ok().map(|s| s.to_lowercase()),
|
||||
env::var(CI_PROJECT_NAMESPACE)
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase()),
|
||||
env::var(CI_PROJECT_NAME).ok().map(|s| s.to_lowercase()),
|
||||
env::var(GITHUB_REPOSITORY_OWNER)
|
||||
.ok()
|
||||
.map(|s| s.to_lowercase()),
|
||||
self.registry.as_ref().map(|s| s.to_lowercase()),
|
||||
self.registry_namespace.as_ref().map(|s| s.to_lowercase()),
|
||||
) {
|
||||
(_, _, _, _, Some(registry), Some(registry_path)) => {
|
||||
trace!("registry={registry}, registry_path={registry_path}");
|
||||
format!(
|
||||
"{}/{}/{}",
|
||||
registry.trim().trim_matches('/'),
|
||||
registry_path.trim().trim_matches('/'),
|
||||
recipe.name.trim(),
|
||||
)
|
||||
}
|
||||
(
|
||||
Some(ci_registry),
|
||||
Some(ci_project_namespace),
|
||||
Some(ci_project_name),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) => {
|
||||
trace!("CI_REGISTRY={ci_registry}, CI_PROJECT_NAMESPACE={ci_project_namespace}, CI_PROJECT_NAME={ci_project_name}");
|
||||
warn!("Generating Gitlab Registry image");
|
||||
format!(
|
||||
"{ci_registry}/{ci_project_namespace}/{ci_project_name}/{}",
|
||||
recipe.name.trim().to_lowercase()
|
||||
)
|
||||
}
|
||||
(None, None, None, Some(github_repository_owner), None, None) => {
|
||||
trace!("GITHUB_REPOSITORY_OWNER={github_repository_owner}");
|
||||
warn!("Generating Github Registry image");
|
||||
format!("ghcr.io/{github_repository_owner}/{}", &recipe.name)
|
||||
}
|
||||
_ => {
|
||||
trace!("Nothing to indicate an image name with a registry");
|
||||
if self.push {
|
||||
bail!("Need '--registry' and '--registry-namespace' in order to push image");
|
||||
}
|
||||
recipe.name.trim().to_lowercase()
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -348,59 +365,6 @@ impl BuildCommand {
|
|||
|
||||
Ok(image_name)
|
||||
}
|
||||
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return `Err` if the build fails.
|
||||
fn run_build(&self, image_name: &str, tags: &[String]) -> Result<()> {
|
||||
trace!("BuildCommand::run_build({image_name}, {tags:#?})");
|
||||
|
||||
let strat = Driver::get_build_driver();
|
||||
|
||||
let full_image = if self.archive.is_some() {
|
||||
image_name.to_string()
|
||||
} else {
|
||||
tags.first()
|
||||
.map_or_else(|| image_name.to_string(), |t| format!("{image_name}:{t}"))
|
||||
};
|
||||
|
||||
info!("Building image {full_image}");
|
||||
strat.build(&full_image)?;
|
||||
|
||||
if tags.len() > 1 && self.archive.is_none() {
|
||||
debug!("Tagging all images");
|
||||
|
||||
for tag in tags {
|
||||
debug!("Tagging {image_name} with {tag}");
|
||||
|
||||
strat.tag(&full_image, image_name, tag)?;
|
||||
|
||||
if self.push {
|
||||
let retry_count = if self.no_retry_push {
|
||||
0
|
||||
} else {
|
||||
self.retry_count
|
||||
};
|
||||
|
||||
debug!("Pushing all images");
|
||||
// Push images with retries (1s delay between retries)
|
||||
blue_build_utils::retry(retry_count, 1000, || {
|
||||
debug!("Pushing image {image_name}:{tag}");
|
||||
|
||||
let tag_image = format!("{image_name}:{tag}");
|
||||
|
||||
strat.push(&tag_image)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.push {
|
||||
sign_images(image_name, tags.first().map(String::as_str))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================== //
|
||||
|
|
@ -413,7 +377,10 @@ fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
|
|||
env::set_var("COSIGN_PASSWORD", "");
|
||||
env::set_var("COSIGN_YES", "true");
|
||||
|
||||
let image_digest = get_image_digest(image_name, tag)?;
|
||||
let image_digest = Driver::get_inspection_driver()
|
||||
.get_metadata(image_name, tag.map_or_else(|| "latest", |t| t))?
|
||||
.digest;
|
||||
let image_name_digest = format!("{image_name}@{image_digest}");
|
||||
let image_name_tag = tag.map_or_else(|| image_name.to_owned(), |t| format!("{image_name}:{t}"));
|
||||
|
||||
match (
|
||||
|
|
@ -433,7 +400,7 @@ fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
|
|||
(_, _, _, _, _, _, _, Ok(cosign_private_key))
|
||||
if !cosign_private_key.is_empty() && Path::new(COSIGN_PATH).exists() =>
|
||||
{
|
||||
sign_priv_public_pair(&image_digest, &image_name_tag)?;
|
||||
sign_priv_public_pair(&image_name_digest, &image_name_tag)?;
|
||||
}
|
||||
// Gitlab keyless
|
||||
(
|
||||
|
|
@ -448,19 +415,19 @@ fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
|
|||
) => {
|
||||
trace!("CI_PROJECT_URL={ci_project_url}, CI_DEFAULT_BRANCH={ci_default_branch}, CI_SERVER_PROTOCOL={ci_server_protocol}, CI_SERVER_HOST={ci_server_host}");
|
||||
|
||||
info!("Signing image: {image_digest}");
|
||||
info!("Signing image: {image_name_digest}");
|
||||
|
||||
trace!("cosign sign {image_digest}");
|
||||
trace!("cosign sign {image_name_digest}");
|
||||
|
||||
if Command::new("cosign")
|
||||
.arg("sign")
|
||||
.arg(&image_digest)
|
||||
.arg(&image_name_digest)
|
||||
.status()?
|
||||
.success()
|
||||
{
|
||||
info!("Successfully signed image!");
|
||||
} else {
|
||||
bail!("Failed to sign image: {image_digest}");
|
||||
bail!("Failed to sign image: {image_name_digest}");
|
||||
}
|
||||
|
||||
let cert_ident =
|
||||
|
|
@ -487,18 +454,18 @@ fn sign_images(image_name: &str, tag: Option<&str>) -> Result<()> {
|
|||
(_, _, _, _, _, Ok(_), Ok(github_worflow_ref), _) => {
|
||||
trace!("GITHUB_WORKFLOW_REF={github_worflow_ref}");
|
||||
|
||||
info!("Signing image {image_digest}");
|
||||
info!("Signing image {image_name_digest}");
|
||||
|
||||
trace!("cosign sign {image_digest}");
|
||||
trace!("cosign sign {image_name_digest}");
|
||||
if Command::new("cosign")
|
||||
.arg("sign")
|
||||
.arg(&image_digest)
|
||||
.arg(&image_name_digest)
|
||||
.status()?
|
||||
.success()
|
||||
{
|
||||
info!("Successfully signed image!");
|
||||
} else {
|
||||
bail!("Failed to sign image: {image_digest}");
|
||||
bail!("Failed to sign image: {image_name_digest}");
|
||||
}
|
||||
|
||||
trace!("cosign verify --certificate-identity-regexp {github_worflow_ref} --certificate-oidc-issuer {GITHUB_TOKEN_ISSUER_URL} {image_name_tag}");
|
||||
|
|
@ -553,30 +520,6 @@ fn sign_priv_public_pair(image_digest: &str, image_name_tag: &str) -> Result<()>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn get_image_digest(image_name: &str, tag: Option<&str>) -> Result<String> {
|
||||
trace!("get_image_digest({image_name}, {tag:?})");
|
||||
|
||||
let image_url = tag.map_or_else(
|
||||
|| format!("docker://{image_name}"),
|
||||
|tag| format!("docker://{image_name}:{tag}"),
|
||||
);
|
||||
|
||||
trace!("skopeo inspect --format='{{.Digest}}' {image_url}");
|
||||
let image_digest = String::from_utf8(
|
||||
Command::new("skopeo")
|
||||
.arg("inspect")
|
||||
.arg("--format='{{.Digest}}'")
|
||||
.arg(&image_url)
|
||||
.output()?
|
||||
.stdout,
|
||||
)?;
|
||||
|
||||
Ok(format!(
|
||||
"{image_name}@{}",
|
||||
image_digest.trim().trim_matches('\'')
|
||||
))
|
||||
}
|
||||
|
||||
fn check_cosign_files() -> Result<()> {
|
||||
trace!("check_for_cosign_files()");
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ impl BlueBuildCommand for UpgradeCommand {
|
|||
.force(self.common.force)
|
||||
.build();
|
||||
|
||||
let image_name = build.generate_full_image_name(&recipe)?;
|
||||
let image_name = recipe.name.to_lowercase().replace('/', "_");
|
||||
|
||||
clean_local_build_dir(&image_name, false)?;
|
||||
debug!("Image name is {image_name}");
|
||||
|
||||
|
|
@ -106,7 +107,7 @@ impl BlueBuildCommand for RebaseCommand {
|
|||
.force(self.common.force)
|
||||
.build();
|
||||
|
||||
let image_name = build.generate_full_image_name(&recipe)?;
|
||||
let image_name = recipe.name.to_lowercase().replace('/', "_");
|
||||
clean_local_build_dir(&image_name, true)?;
|
||||
debug!("Image name is {image_name}");
|
||||
|
||||
|
|
@ -158,7 +159,7 @@ fn clean_local_build_dir(image_name: &str, rebase: bool) -> Result<()> {
|
|||
trace!("clean_local_build_dir()");
|
||||
|
||||
let local_build_path = Path::new(LOCAL_BUILD);
|
||||
let image_file_path = local_build_path.join(image_name.trim_start_matches("oci-archive:"));
|
||||
let image_file_path = local_build_path.join(format!("{image_name}.{ARCHIVE_SUFFIX}"));
|
||||
|
||||
if !image_file_path.exists() && !rebase {
|
||||
bail!(
|
||||
|
|
|
|||
|
|
@ -33,15 +33,16 @@ use tokio::runtime::Runtime;
|
|||
#[cfg(feature = "builtin-podman")]
|
||||
use podman_api_driver::PodmanApiDriver;
|
||||
|
||||
use crate::{credentials, image_inspection::ImageInspection};
|
||||
use crate::{credentials, image_metadata::ImageMetadata};
|
||||
|
||||
use self::{
|
||||
buildah_driver::BuildahDriver, docker_driver::DockerDriver, podman_driver::PodmanDriver,
|
||||
skopeo_driver::SkopeoDriver,
|
||||
buildah_driver::BuildahDriver, docker_driver::DockerDriver, opts::BuildTagPushOpts,
|
||||
podman_driver::PodmanDriver, skopeo_driver::SkopeoDriver,
|
||||
};
|
||||
|
||||
mod buildah_driver;
|
||||
mod docker_driver;
|
||||
pub mod opts;
|
||||
#[cfg(feature = "builtin-podman")]
|
||||
mod podman_api_driver;
|
||||
mod podman_driver;
|
||||
|
|
@ -139,15 +140,72 @@ pub trait BuildDriver: Sync + Send {
|
|||
/// # Errors
|
||||
/// Will error if login fails.
|
||||
fn login(&self) -> Result<()>;
|
||||
|
||||
/// Runs the logic for building, tagging, and pushing an image.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if building, tagging, or pusing fails.
|
||||
fn build_tag_push(&self, 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"),
|
||||
};
|
||||
|
||||
info!("Building image {full_image}");
|
||||
self.build(&full_image)?;
|
||||
|
||||
if !opts.tags.is_empty() && opts.archive_path.is_none() {
|
||||
let image = opts
|
||||
.image
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow!("Image is required in order to tag"))?;
|
||||
debug!("Tagging all images");
|
||||
|
||||
for tag in opts.tags.as_ref() {
|
||||
debug!("Tagging {} with {tag}", &full_image);
|
||||
|
||||
self.tag(&full_image, image.as_ref(), tag)?;
|
||||
|
||||
if opts.push {
|
||||
let retry_count = if opts.no_retry_push {
|
||||
0
|
||||
} else {
|
||||
opts.retry_count
|
||||
};
|
||||
|
||||
debug!("Pushing all images");
|
||||
// Push images with retries (1s delay between retries)
|
||||
blue_build_utils::retry(retry_count, 1000, || {
|
||||
let tag_image = format!("{image}:{tag}");
|
||||
|
||||
debug!("Pushing image {tag_image}");
|
||||
|
||||
self.push(&tag_image)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Allows agnostic inspection of images.
|
||||
pub trait InspectDriver: Sync + Send {
|
||||
/// Gets the labels on an image tag.
|
||||
/// Gets the metadata on an image tag.
|
||||
///
|
||||
/// # Errors
|
||||
/// Will error if it is unable to get the labels.
|
||||
fn get_labels(&self, image_name: &str, tag: &str) -> Result<ImageInspection>;
|
||||
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata>;
|
||||
}
|
||||
|
||||
#[derive(Debug, TypedBuilder)]
|
||||
|
|
@ -172,6 +230,7 @@ impl Driver<'_> {
|
|||
/// # Errors
|
||||
/// Will error if it is unable to set the user credentials.
|
||||
pub fn init(self) -> Result<()> {
|
||||
trace!("Driver::init()");
|
||||
credentials::set_user_creds(self.username, self.password, self.registry)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -179,16 +238,19 @@ impl Driver<'_> {
|
|||
/// Gets the current build's UUID
|
||||
#[must_use]
|
||||
pub fn get_build_id() -> Uuid {
|
||||
trace!("Driver::get_build_id()");
|
||||
*BUILD_ID
|
||||
}
|
||||
|
||||
/// Gets the current run's build strategy
|
||||
pub fn get_build_driver() -> Arc<dyn BuildDriver> {
|
||||
trace!("Driver::get_build_driver()");
|
||||
BUILD_STRATEGY.clone()
|
||||
}
|
||||
|
||||
/// Gets the current run's inspectioin strategy
|
||||
pub fn get_inspection_driver() -> Arc<dyn InspectDriver> {
|
||||
trace!("Driver::get_inspection_driver()");
|
||||
INSPECT_STRATEGY.clone()
|
||||
}
|
||||
|
||||
|
|
@ -201,7 +263,7 @@ impl Driver<'_> {
|
|||
/// Will error if the image doesn't have OS version info
|
||||
/// or we are unable to lock a mutex.
|
||||
pub fn get_os_version(recipe: &Recipe) -> Result<String> {
|
||||
trace!("get_os_version({recipe:#?})");
|
||||
trace!("Driver::get_os_version({recipe:#?})");
|
||||
let image = format!("{}:{}", &recipe.base_image, &recipe.image_version);
|
||||
|
||||
let mut os_version_lock = OS_VERSION
|
||||
|
|
@ -214,7 +276,7 @@ impl Driver<'_> {
|
|||
None => {
|
||||
info!("Retrieving OS version from {image}. This might take a bit");
|
||||
let inspection =
|
||||
INSPECT_STRATEGY.get_labels(&recipe.base_image, &recipe.image_version)?;
|
||||
INSPECT_STRATEGY.get_metadata(&recipe.base_image, &recipe.image_version)?;
|
||||
|
||||
let os_version = inspection.get_version().ok_or_else(|| {
|
||||
anyhow!(
|
||||
|
|
@ -240,7 +302,7 @@ impl Driver<'_> {
|
|||
}
|
||||
|
||||
fn determine_inspect_driver() -> Result<Arc<dyn InspectDriver>> {
|
||||
trace!("Strategy::determine_inspect_strategy()");
|
||||
trace!("Driver::determine_inspect_driver()");
|
||||
|
||||
let driver: Arc<dyn InspectDriver> = match (
|
||||
blue_build_utils::check_command_exists("skopeo"),
|
||||
|
|
@ -257,7 +319,7 @@ impl Driver<'_> {
|
|||
}
|
||||
|
||||
fn determine_build_driver() -> Result<Arc<dyn BuildDriver>> {
|
||||
trace!("Strategy::determine_build_strategy()");
|
||||
trace!("Driver::determine_build_driver()");
|
||||
|
||||
let driver: Arc<dyn BuildDriver> = match (
|
||||
env::var(XDG_RUNTIME_DIR),
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ use std::{
|
|||
};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use blue_build_utils::constants::{BB_BUILDKIT_CACHE_GHA, SKOPEO_IMAGE};
|
||||
use blue_build_utils::constants::{BB_BUILDKIT_CACHE_GHA, CONTAINER_FILE, SKOPEO_IMAGE};
|
||||
use log::{info, trace};
|
||||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::image_inspection::ImageInspection;
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
|
||||
use super::{credentials, BuildDriver, DriverVersion, InspectDriver};
|
||||
use super::{credentials, opts::BuildTagPushOpts, BuildDriver, DriverVersion, InspectDriver};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DockerVerisonJsonClient {
|
||||
|
|
@ -48,34 +48,19 @@ impl DriverVersion for DockerDriver {
|
|||
|
||||
impl BuildDriver for DockerDriver {
|
||||
fn build(&self, image: &str) -> Result<()> {
|
||||
trace!("docker");
|
||||
let mut command = Command::new("docker");
|
||||
trace!("DockerDriver::build({image})");
|
||||
|
||||
// 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") {
|
||||
trace!("buildx build --load --cache-from type=gha --cache-to type=gha");
|
||||
command
|
||||
.arg("buildx")
|
||||
.arg("build")
|
||||
.arg("--load")
|
||||
.arg("--cache-from")
|
||||
.arg("type=gha")
|
||||
.arg("--cache-to")
|
||||
.arg("type=gha");
|
||||
} else {
|
||||
trace!("build");
|
||||
command.arg("build");
|
||||
}
|
||||
|
||||
trace!("-t {image} -f Containerfile .");
|
||||
command
|
||||
trace!("docker build -t {image} -f {CONTAINER_FILE} .");
|
||||
let status = Command::new("docker")
|
||||
.arg("build")
|
||||
.arg("-t")
|
||||
.arg(image)
|
||||
.arg("-f")
|
||||
.arg("Containerfile")
|
||||
.arg(".");
|
||||
.arg(CONTAINER_FILE)
|
||||
.arg(".")
|
||||
.status()?;
|
||||
|
||||
if command.status()?.success() {
|
||||
if status.success() {
|
||||
info!("Successfully built {image}");
|
||||
} else {
|
||||
bail!("Failed to build {image}");
|
||||
|
|
@ -84,6 +69,8 @@ impl BuildDriver for DockerDriver {
|
|||
}
|
||||
|
||||
fn tag(&self, src_image: &str, image_name: &str, tag: &str) -> Result<()> {
|
||||
trace!("DockerDriver::tag({src_image}, {image_name}, {tag})");
|
||||
|
||||
let dest_image = format!("{image_name}:{tag}");
|
||||
|
||||
trace!("docker tag {src_image} {dest_image}");
|
||||
|
|
@ -102,6 +89,8 @@ impl BuildDriver for DockerDriver {
|
|||
}
|
||||
|
||||
fn push(&self, image: &str) -> Result<()> {
|
||||
trace!("DockerDriver::push({image})");
|
||||
|
||||
trace!("docker push {image}");
|
||||
let status = Command::new("docker").arg("push").arg(image).status()?;
|
||||
|
||||
|
|
@ -114,6 +103,8 @@ impl BuildDriver for DockerDriver {
|
|||
}
|
||||
|
||||
fn login(&self) -> Result<()> {
|
||||
trace!("DockerDriver::login()");
|
||||
|
||||
let (registry, username, password) =
|
||||
credentials::get().map(|c| (&c.registry, &c.username, &c.password))?;
|
||||
|
||||
|
|
@ -129,14 +120,85 @@ impl BuildDriver for DockerDriver {
|
|||
|
||||
if !output.status.success() {
|
||||
let err_out = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to login for buildah: {err_out}");
|
||||
bail!("Failed to login for docker: {err_out}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_tag_push(&self, opts: &BuildTagPushOpts) -> Result<()> {
|
||||
trace!("DockerDriver::build_tag_push({opts:#?})");
|
||||
|
||||
let mut command = Command::new("docker");
|
||||
|
||||
trace!("docker buildx build -f {CONTAINER_FILE}");
|
||||
command
|
||||
.arg("buildx")
|
||||
.arg("build")
|
||||
.arg("-f")
|
||||
.arg(CONTAINER_FILE);
|
||||
|
||||
// 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") {
|
||||
trace!("--cache-from type=gha --cache-to type=gha");
|
||||
command
|
||||
.arg("--cache-from")
|
||||
.arg("type=gha")
|
||||
.arg("--cache-to")
|
||||
.arg("type=gha");
|
||||
}
|
||||
|
||||
match (opts.image.as_ref(), opts.archive_path.as_ref()) {
|
||||
(Some(image), None) => {
|
||||
if opts.tags.is_empty() {
|
||||
trace!("-t {image}");
|
||||
command.arg("-t").arg(image.as_ref());
|
||||
} else {
|
||||
for tag in opts.tags.as_ref() {
|
||||
let full_image = format!("{image}:{tag}");
|
||||
|
||||
trace!("-t {full_image}");
|
||||
command.arg("-t").arg(full_image);
|
||||
}
|
||||
}
|
||||
|
||||
if opts.push {
|
||||
trace!("--push");
|
||||
command.arg("--push");
|
||||
} else {
|
||||
trace!("--builder default");
|
||||
command.arg("--builder").arg("default");
|
||||
}
|
||||
}
|
||||
(None, Some(archive_path)) => {
|
||||
trace!("--output type=oci,dest={archive_path}");
|
||||
command
|
||||
.arg("--output")
|
||||
.arg(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"),
|
||||
}
|
||||
|
||||
trace!(".");
|
||||
command.arg(".");
|
||||
|
||||
if command.status()?.success() {
|
||||
if opts.push {
|
||||
info!("Successfully built and pushed image");
|
||||
} else {
|
||||
info!("Successfully built image");
|
||||
}
|
||||
} else {
|
||||
bail!("Failed to build image");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectDriver for DockerDriver {
|
||||
fn get_labels(&self, image_name: &str, tag: &str) -> Result<ImageInspection> {
|
||||
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata> {
|
||||
trace!("DockerDriver::get_labels({image_name}, {tag})");
|
||||
|
||||
let url = format!("docker://{image_name}:{tag}");
|
||||
|
||||
trace!("docker run {SKOPEO_IMAGE} inspect {url}");
|
||||
|
|
|
|||
3
src/drivers/opts.rs
Normal file
3
src/drivers/opts.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
mod build;
|
||||
|
||||
pub use build::*;
|
||||
37
src/drivers/opts/build.rs
Normal file
37
src/drivers/opts/build.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use typed_builder::TypedBuilder;
|
||||
|
||||
/// Options for building, tagging, and pusing images.
|
||||
#[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 list of tags for the image being built.
|
||||
#[builder(default, setter(into))]
|
||||
pub tags: Cow<'a, [&'a str]>,
|
||||
|
||||
/// Enable pushing the image.
|
||||
#[builder(default)]
|
||||
pub push: bool,
|
||||
|
||||
/// Disable retry logic for pushing.
|
||||
#[builder(default)]
|
||||
pub no_retry_push: bool,
|
||||
|
||||
/// Number of times to retry pushing.
|
||||
///
|
||||
/// Defaults to 1.
|
||||
#[builder(default = 1)]
|
||||
pub retry_count: u8,
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@ use log::{debug, info, trace};
|
|||
use semver::Version;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::image_inspection::ImageInspection;
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
|
||||
use super::{credentials, BuildDriver, DriverVersion, InspectDriver};
|
||||
|
||||
|
|
@ -107,14 +107,14 @@ impl BuildDriver for PodmanDriver {
|
|||
|
||||
if !output.status.success() {
|
||||
let err_out = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Failed to login for buildah: {err_out}");
|
||||
bail!("Failed to login for podman: {err_out}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl InspectDriver for PodmanDriver {
|
||||
fn get_labels(&self, image_name: &str, tag: &str) -> Result<ImageInspection> {
|
||||
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata> {
|
||||
let url = format!("docker://{image_name}:{tag}");
|
||||
|
||||
trace!("podman run {SKOPEO_IMAGE} inspect {url}");
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::process::{Command, Stdio};
|
|||
use anyhow::{bail, Result};
|
||||
use log::{debug, trace};
|
||||
|
||||
use crate::image_inspection::ImageInspection;
|
||||
use crate::image_metadata::ImageMetadata;
|
||||
|
||||
use super::InspectDriver;
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ use super::InspectDriver;
|
|||
pub struct SkopeoDriver;
|
||||
|
||||
impl InspectDriver for SkopeoDriver {
|
||||
fn get_labels(&self, image_name: &str, tag: &str) -> Result<ImageInspection> {
|
||||
fn get_metadata(&self, image_name: &str, tag: &str) -> Result<ImageMetadata> {
|
||||
let url = format!("docker://{image_name}:{tag}");
|
||||
|
||||
trace!("skopeo inspect {url}");
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ use serde_json::Value;
|
|||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct ImageInspection {
|
||||
pub struct ImageMetadata {
|
||||
#[serde(alias = "Labels")]
|
||||
labels: HashMap<String, Value>,
|
||||
pub labels: HashMap<String, Value>,
|
||||
|
||||
#[serde(alias = "Digest")]
|
||||
pub digest: String,
|
||||
}
|
||||
|
||||
impl ImageInspection {
|
||||
impl ImageMetadata {
|
||||
pub fn get_version(&self) -> Option<String> {
|
||||
Some(
|
||||
self.labels
|
||||
|
|
@ -7,4 +7,4 @@ shadow_rs::shadow!(shadow);
|
|||
pub mod commands;
|
||||
pub mod credentials;
|
||||
pub mod drivers;
|
||||
pub mod image_inspection;
|
||||
pub mod image_metadata;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue