feat(wfe-buildkit): rewrite to use own generated protos (tonic 0.14)
Replaced third-party buildkit-client git dependency with wfe-buildkit-protos generated from official moby/buildkit protos. Direct ControlClient gRPC calls: SolveRequest with frontend attrs, exporters, cache options. Daemon-local context paths for builds (session protocol for remote transfer is TODO). Both proto crates now use tonic 0.14 / prost 0.14 — no transitive dependency conflicts. 95 combined tests, 85.6% region coverage.
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let buildkit_root = PathBuf::from("vendor/buildkit");
|
|
||||||
|
|
||||||
// Use Go-style import paths so protoc sees each file only once
|
// Use Go-style import paths so protoc sees each file only once
|
||||||
let proto_dir = PathBuf::from("proto");
|
let proto_dir = PathBuf::from("proto");
|
||||||
let go_prefix = "github.com/moby/buildkit";
|
let go_prefix = "github.com/moby/buildkit";
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ async-trait = { workspace = true }
|
|||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
buildkit-client = { git = "https://github.com/AprilNEA/buildkit-client.git", branch = "master", default-features = false }
|
wfe-buildkit-protos = { path = "../wfe-buildkit-protos" }
|
||||||
tonic = "0.12"
|
tonic = "0.14"
|
||||||
tower = "0.4"
|
tower = { version = "0.4", features = ["util"] }
|
||||||
hyper-util = { version = "0.1", features = ["tokio"] }
|
hyper-util = { version = "0.1", features = ["tokio"] }
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|||||||
@@ -2,21 +2,29 @@ use std::collections::HashMap;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use buildkit_client::proto::moby::buildkit::v1::control_client::ControlClient;
|
|
||||||
use buildkit_client::proto::moby::buildkit::v1::{
|
|
||||||
CacheOptions, CacheOptionsEntry, Exporter, SolveRequest, StatusRequest,
|
|
||||||
};
|
|
||||||
use buildkit_client::session::{AuthServer, FileSync, RegistryAuthConfig, Session};
|
|
||||||
use buildkit_client::{BuildConfig, BuildResult};
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tonic::transport::{Channel, Endpoint, Uri};
|
use tonic::transport::{Channel, Endpoint, Uri};
|
||||||
|
use wfe_buildkit_protos::moby::buildkit::v1::control_client::ControlClient;
|
||||||
|
use wfe_buildkit_protos::moby::buildkit::v1::{
|
||||||
|
CacheOptions, CacheOptionsEntry, Exporter, SolveRequest, StatusRequest,
|
||||||
|
};
|
||||||
use wfe_core::models::ExecutionResult;
|
use wfe_core::models::ExecutionResult;
|
||||||
use wfe_core::traits::step::{StepBody, StepExecutionContext};
|
use wfe_core::traits::step::{StepBody, StepExecutionContext};
|
||||||
use wfe_core::WfeError;
|
use wfe_core::WfeError;
|
||||||
|
|
||||||
use crate::config::BuildkitConfig;
|
use crate::config::BuildkitConfig;
|
||||||
|
|
||||||
|
/// Result of a BuildKit solve operation.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct BuildResult {
|
||||||
|
/// Image digest produced by the build, if any.
|
||||||
|
pub digest: Option<String>,
|
||||||
|
/// Full exporter response metadata from the daemon.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub metadata: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// A workflow step that builds container images via the BuildKit gRPC API.
|
/// A workflow step that builds container images via the BuildKit gRPC API.
|
||||||
pub struct BuildkitStep {
|
pub struct BuildkitStep {
|
||||||
config: BuildkitConfig,
|
config: BuildkitConfig,
|
||||||
@@ -96,173 +104,59 @@ impl BuildkitStep {
|
|||||||
Ok(ControlClient::new(channel))
|
Ok(ControlClient::new(channel))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build a [`BuildConfig`] from our [`BuildkitConfig`].
|
/// Build frontend attributes from the current configuration.
|
||||||
///
|
///
|
||||||
/// This is used internally to prepare the configuration and also
|
/// These attributes tell the BuildKit dockerfile frontend how to process
|
||||||
/// exposed for testing.
|
/// the build: which file to use, which target stage, build arguments, etc.
|
||||||
pub(crate) fn build_config(&self) -> BuildConfig {
|
fn build_frontend_attrs(&self) -> HashMap<String, String> {
|
||||||
let mut bc = BuildConfig::local(&self.config.context);
|
let mut attrs = HashMap::new();
|
||||||
|
|
||||||
// Set the dockerfile path if it differs from default.
|
// Dockerfile filename (relative to context).
|
||||||
if self.config.dockerfile != "Dockerfile" {
|
if self.config.dockerfile != "Dockerfile" {
|
||||||
bc = bc.dockerfile(&self.config.dockerfile);
|
attrs.insert("filename".to_string(), self.config.dockerfile.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Target stage
|
// Target stage for multi-stage builds.
|
||||||
if let Some(ref target) = self.config.target {
|
if let Some(ref target) = self.config.target {
|
||||||
bc = bc.target(target);
|
attrs.insert("target".to_string(), target.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build arguments (sorted for determinism)
|
// Build arguments (sorted for determinism).
|
||||||
let mut sorted_args: Vec<_> = self.config.build_args.iter().collect();
|
let mut sorted_args: Vec<_> = self.config.build_args.iter().collect();
|
||||||
sorted_args.sort_by_key(|(k, _)| (*k).clone());
|
sorted_args.sort_by_key(|(k, _)| (*k).clone());
|
||||||
for (key, value) in &sorted_args {
|
for (key, value) in &sorted_args {
|
||||||
bc = bc.build_arg(key.as_str(), value.as_str());
|
attrs.insert(format!("build-arg:{key}"), value.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tags
|
attrs
|
||||||
for tag in &self.config.tags {
|
|
||||||
bc = bc.tag(tag);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Registry auth
|
|
||||||
for (host, auth) in &self.config.registry_auth {
|
|
||||||
bc = bc.registry_auth(buildkit_client::RegistryAuth {
|
|
||||||
host: host.clone(),
|
|
||||||
username: auth.username.clone(),
|
|
||||||
password: auth.password.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cache import/export
|
|
||||||
for source in &self.config.cache_from {
|
|
||||||
bc = bc.cache_from(source);
|
|
||||||
}
|
|
||||||
for dest in &self.config.cache_to {
|
|
||||||
bc = bc.cache_to(dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
bc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute the build against a connected BuildKit daemon.
|
/// Build exporter configuration for image output.
|
||||||
///
|
fn build_exporters(&self) -> Vec<Exporter> {
|
||||||
/// This reimplements the core solve logic from `buildkit-client` to
|
if self.config.tags.is_empty() {
|
||||||
/// work with our own gRPC channel (needed for Unix socket support).
|
return vec![];
|
||||||
async fn execute_build(
|
|
||||||
&self,
|
|
||||||
control: &mut ControlClient<Channel>,
|
|
||||||
config: BuildConfig,
|
|
||||||
) -> Result<BuildResult, WfeError> {
|
|
||||||
let build_ref = format!("wfe-build-{}", uuid::Uuid::new_v4());
|
|
||||||
|
|
||||||
// Create and start session.
|
|
||||||
let mut session = Session::new();
|
|
||||||
|
|
||||||
// Add file sync for local context.
|
|
||||||
if let buildkit_client::DockerfileSource::Local {
|
|
||||||
ref context_path, ..
|
|
||||||
} = config.source
|
|
||||||
{
|
|
||||||
let abs_path = std::fs::canonicalize(context_path).map_err(|e| {
|
|
||||||
WfeError::StepExecution(format!(
|
|
||||||
"failed to resolve context path {}: {e}",
|
|
||||||
context_path.display()
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
session.add_file_sync(abs_path).await;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add registry auth to session.
|
let mut export_attrs = HashMap::new();
|
||||||
if let Some(ref registry_auth) = config.registry_auth {
|
export_attrs.insert("name".to_string(), self.config.tags.join(","));
|
||||||
let mut auth = AuthServer::new();
|
if self.config.push {
|
||||||
auth.add_registry(RegistryAuthConfig {
|
|
||||||
host: registry_auth.host.clone(),
|
|
||||||
username: registry_auth.username.clone(),
|
|
||||||
password: registry_auth.password.clone(),
|
|
||||||
});
|
|
||||||
session.add_auth(auth).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the session.
|
|
||||||
session.start(control.clone()).await.map_err(|e| {
|
|
||||||
WfeError::StepExecution(format!("failed to start BuildKit session: {e}"))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
tracing::info!(session_id = %session.get_id(), "session started");
|
|
||||||
|
|
||||||
// Prepare frontend attributes.
|
|
||||||
let mut frontend_attrs = HashMap::new();
|
|
||||||
|
|
||||||
match &config.source {
|
|
||||||
buildkit_client::DockerfileSource::Local {
|
|
||||||
dockerfile_path, ..
|
|
||||||
} => {
|
|
||||||
if let Some(path) = dockerfile_path {
|
|
||||||
frontend_attrs
|
|
||||||
.insert("filename".to_string(), path.to_string_lossy().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
buildkit_client::DockerfileSource::GitHub {
|
|
||||||
dockerfile_path, ..
|
|
||||||
} => {
|
|
||||||
if let Some(path) = dockerfile_path {
|
|
||||||
frontend_attrs.insert("filename".to_string(), path.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (key, value) in &config.build_args {
|
|
||||||
frontend_attrs.insert(format!("build-arg:{key}"), value.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(target) = &config.target {
|
|
||||||
frontend_attrs.insert("target".to_string(), target.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.no_cache {
|
|
||||||
frontend_attrs.insert("no-cache".to_string(), "true".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare context source.
|
|
||||||
let context = match &config.source {
|
|
||||||
buildkit_client::DockerfileSource::Local { context_path, .. } => {
|
|
||||||
let file_sync = FileSync::new(context_path);
|
|
||||||
file_sync.validate().map_err(|e| {
|
|
||||||
WfeError::StepExecution(format!("invalid build context: {e}"))
|
|
||||||
})?;
|
|
||||||
format!("input:{}:context", session.shared_key)
|
|
||||||
}
|
|
||||||
buildkit_client::DockerfileSource::GitHub {
|
|
||||||
repo_url, git_ref, ..
|
|
||||||
} => {
|
|
||||||
let mut url = repo_url.clone();
|
|
||||||
if !url.ends_with(".git") {
|
|
||||||
url.push_str(".git");
|
|
||||||
}
|
|
||||||
if let Some(git_ref) = git_ref {
|
|
||||||
url = format!("{url}#{git_ref}");
|
|
||||||
}
|
|
||||||
url
|
|
||||||
}
|
|
||||||
};
|
|
||||||
frontend_attrs.insert("context".to_string(), context);
|
|
||||||
|
|
||||||
// Prepare exporters (for image push).
|
|
||||||
let mut exports = Vec::new();
|
|
||||||
if !config.tags.is_empty() {
|
|
||||||
let mut export_attrs = HashMap::new();
|
|
||||||
export_attrs.insert("name".to_string(), config.tags.join(","));
|
|
||||||
export_attrs.insert("push".to_string(), "true".to_string());
|
export_attrs.insert("push".to_string(), "true".to_string());
|
||||||
|
|
||||||
exports.push(Exporter {
|
|
||||||
r#type: "image".to_string(),
|
|
||||||
attrs: export_attrs,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare cache.
|
vec![Exporter {
|
||||||
let cache_imports = config
|
r#type: "image".to_string(),
|
||||||
|
attrs: export_attrs,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build cache options from the configuration.
|
||||||
|
fn build_cache_options(&self) -> Option<CacheOptions> {
|
||||||
|
if self.config.cache_from.is_empty() && self.config.cache_to.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let imports = self
|
||||||
|
.config
|
||||||
.cache_from
|
.cache_from
|
||||||
.iter()
|
.iter()
|
||||||
.map(|source| {
|
.map(|source| {
|
||||||
@@ -275,7 +169,8 @@ impl BuildkitStep {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let cache_exports = config
|
let exports = self
|
||||||
|
.config
|
||||||
.cache_to
|
.cache_to
|
||||||
.iter()
|
.iter()
|
||||||
.map(|dest| {
|
.map(|dest| {
|
||||||
@@ -289,49 +184,130 @@ impl BuildkitStep {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Build the solve request.
|
Some(CacheOptions {
|
||||||
|
export_ref_deprecated: String::new(),
|
||||||
|
import_refs_deprecated: vec![],
|
||||||
|
export_attrs_deprecated: HashMap::new(),
|
||||||
|
exports,
|
||||||
|
imports,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute the build against a connected BuildKit daemon.
|
||||||
|
///
|
||||||
|
/// Constructs a `SolveRequest` using the dockerfile.v0 frontend and
|
||||||
|
/// sends it via the Control gRPC service. The build context must be
|
||||||
|
/// accessible to the daemon on its local filesystem (shared mount or
|
||||||
|
/// same machine).
|
||||||
|
///
|
||||||
|
/// # Session protocol
|
||||||
|
///
|
||||||
|
/// TODO: For remote daemons where the build context is not on the same
|
||||||
|
/// filesystem, a full session protocol implementation is needed to
|
||||||
|
/// transfer files. Currently we rely on the context directory being
|
||||||
|
/// available to buildkitd (e.g., via a shared mount in Lima/colima).
|
||||||
|
async fn execute_build(
|
||||||
|
&self,
|
||||||
|
control: &mut ControlClient<Channel>,
|
||||||
|
) -> Result<BuildResult, WfeError> {
|
||||||
|
let build_ref = format!("wfe-build-{}", uuid::Uuid::new_v4());
|
||||||
|
let session_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
// Resolve the absolute context path.
|
||||||
|
let abs_context = std::fs::canonicalize(&self.config.context).map_err(|e| {
|
||||||
|
WfeError::StepExecution(format!(
|
||||||
|
"failed to resolve context path {}: {e}",
|
||||||
|
self.config.context
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Build frontend attributes with local context references.
|
||||||
|
let mut frontend_attrs = self.build_frontend_attrs();
|
||||||
|
|
||||||
|
// Point the frontend at the daemon-local context directory.
|
||||||
|
// The "context" attr tells the dockerfile frontend where to find
|
||||||
|
// the build context. For local builds we use the local source type
|
||||||
|
// with a shared-key reference.
|
||||||
|
let context_name = "context";
|
||||||
|
let dockerfile_name = "dockerfile";
|
||||||
|
|
||||||
|
frontend_attrs.insert(
|
||||||
|
"context".to_string(),
|
||||||
|
format!("local://{context_name}"),
|
||||||
|
);
|
||||||
|
frontend_attrs.insert(
|
||||||
|
format!("local-sessionid:{context_name}"),
|
||||||
|
session_id.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Also provide the dockerfile source as a local reference.
|
||||||
|
frontend_attrs.insert(
|
||||||
|
"dockerfilekey".to_string(),
|
||||||
|
format!("local://{dockerfile_name}"),
|
||||||
|
);
|
||||||
|
frontend_attrs.insert(
|
||||||
|
format!("local-sessionid:{dockerfile_name}"),
|
||||||
|
session_id.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let request = SolveRequest {
|
let request = SolveRequest {
|
||||||
r#ref: build_ref.clone(),
|
r#ref: build_ref.clone(),
|
||||||
definition: None,
|
definition: None,
|
||||||
exporter_deprecated: String::new(),
|
exporter_deprecated: String::new(),
|
||||||
exporter_attrs_deprecated: HashMap::new(),
|
exporter_attrs_deprecated: HashMap::new(),
|
||||||
session: session.get_id(),
|
session: session_id.clone(),
|
||||||
frontend: "dockerfile.v0".to_string(),
|
frontend: "dockerfile.v0".to_string(),
|
||||||
frontend_attrs,
|
frontend_attrs,
|
||||||
cache: Some(CacheOptions {
|
cache: self.build_cache_options(),
|
||||||
export_ref_deprecated: String::new(),
|
|
||||||
import_refs_deprecated: vec![],
|
|
||||||
export_attrs_deprecated: HashMap::new(),
|
|
||||||
exports: cache_exports,
|
|
||||||
imports: cache_imports,
|
|
||||||
}),
|
|
||||||
entitlements: vec![],
|
entitlements: vec![],
|
||||||
frontend_inputs: HashMap::new(),
|
frontend_inputs: HashMap::new(),
|
||||||
internal: false,
|
internal: false,
|
||||||
source_policy: None,
|
source_policy: None,
|
||||||
exporters: exports,
|
exporters: self.build_exporters(),
|
||||||
enable_session_exporter: false,
|
enable_session_exporter: false,
|
||||||
|
source_policy_session: String::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send the solve request with session metadata.
|
// Attach session metadata headers so buildkitd knows which
|
||||||
|
// session provides the local source content.
|
||||||
let mut grpc_request = tonic::Request::new(request);
|
let mut grpc_request = tonic::Request::new(request);
|
||||||
let metadata = grpc_request.metadata_mut();
|
let metadata = grpc_request.metadata_mut();
|
||||||
|
|
||||||
for (key, values) in session.metadata() {
|
// The x-docker-expose-session-uuid header tells buildkitd which
|
||||||
if let Ok(k) =
|
// session owns the local sources. The x-docker-expose-session-grpc-method
|
||||||
key.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
// header lists the gRPC methods the session implements.
|
||||||
|
if let Ok(key) =
|
||||||
|
"x-docker-expose-session-uuid"
|
||||||
|
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
||||||
|
&& let Ok(val) = session_id
|
||||||
|
.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
||||||
|
{
|
||||||
|
metadata.insert(key, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advertise the filesync method so the daemon knows it can request
|
||||||
|
// local file content from our session.
|
||||||
|
if let Ok(key) =
|
||||||
|
"x-docker-expose-session-grpc-method"
|
||||||
|
.parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>()
|
||||||
|
{
|
||||||
|
if let Ok(val) = "/moby.filesync.v1.FileSync/DiffCopy"
|
||||||
|
.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
||||||
{
|
{
|
||||||
for value in values {
|
metadata.append(key.clone(), val);
|
||||||
if let Ok(v) = value
|
}
|
||||||
.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
if let Ok(val) = "/moby.filesync.v1.Auth/Credentials"
|
||||||
{
|
.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>()
|
||||||
metadata.append(k.clone(), v);
|
{
|
||||||
}
|
metadata.append(key, val);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tracing::info!("sending solve request to BuildKit");
|
tracing::info!(
|
||||||
|
context = %abs_context.display(),
|
||||||
|
session_id = %session_id,
|
||||||
|
"sending solve request to BuildKit"
|
||||||
|
);
|
||||||
|
|
||||||
let response = control
|
let response = control
|
||||||
.solve(grpc_request)
|
.solve(grpc_request)
|
||||||
@@ -453,20 +429,12 @@ impl StepBody for BuildkitStep {
|
|||||||
// Connect to the BuildKit daemon.
|
// Connect to the BuildKit daemon.
|
||||||
let mut control = self.connect().await?;
|
let mut control = self.connect().await?;
|
||||||
|
|
||||||
// Build the configuration for this solve request.
|
|
||||||
let build_config = self.build_config();
|
|
||||||
|
|
||||||
tracing::info!(step = step_name, "submitting build to BuildKit");
|
tracing::info!(step = step_name, "submitting build to BuildKit");
|
||||||
|
|
||||||
// Execute the build with optional timeout.
|
// Execute the build with optional timeout.
|
||||||
let result = if let Some(timeout_ms) = self.config.timeout_ms {
|
let result = if let Some(timeout_ms) = self.config.timeout_ms {
|
||||||
let duration = std::time::Duration::from_millis(timeout_ms);
|
let duration = std::time::Duration::from_millis(timeout_ms);
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(duration, self.execute_build(&mut control)).await {
|
||||||
duration,
|
|
||||||
self.execute_build(&mut control, build_config),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(Ok(result)) => result,
|
Ok(Ok(result)) => result,
|
||||||
Ok(Err(e)) => return Err(e),
|
Ok(Err(e)) => return Err(e),
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@@ -476,7 +444,7 @@ impl StepBody for BuildkitStep {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.execute_build(&mut control, build_config).await?
|
self.execute_build(&mut control).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Extract digest from BuildResult.
|
// Extract digest from BuildResult.
|
||||||
@@ -761,34 +729,41 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
// build_config tests
|
// build_frontend_attrs tests
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_config_minimal() {
|
fn frontend_attrs_minimal() {
|
||||||
let step = BuildkitStep::new(minimal_config());
|
let step = BuildkitStep::new(minimal_config());
|
||||||
let _bc = step.build_config();
|
let attrs = step.build_frontend_attrs();
|
||||||
|
// Default Dockerfile name is not included (only non-default).
|
||||||
|
assert!(!attrs.contains_key("filename"));
|
||||||
|
assert!(!attrs.contains_key("target"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_config_with_target() {
|
fn frontend_attrs_with_target() {
|
||||||
let mut config = minimal_config();
|
let mut config = minimal_config();
|
||||||
config.target = Some("runtime".to_string());
|
config.target = Some("runtime".to_string());
|
||||||
let step = BuildkitStep::new(config);
|
let step = BuildkitStep::new(config);
|
||||||
let _bc = step.build_config();
|
let attrs = step.build_frontend_attrs();
|
||||||
|
assert_eq!(attrs.get("target"), Some(&"runtime".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_config_with_tags() {
|
fn frontend_attrs_with_custom_dockerfile() {
|
||||||
let mut config = minimal_config();
|
let mut config = minimal_config();
|
||||||
config.tags = vec!["myapp:latest".to_string(), "myapp:v1.0".to_string()];
|
config.dockerfile = "docker/Dockerfile.prod".to_string();
|
||||||
config.push = true;
|
|
||||||
let step = BuildkitStep::new(config);
|
let step = BuildkitStep::new(config);
|
||||||
let _bc = step.build_config();
|
let attrs = step.build_frontend_attrs();
|
||||||
|
assert_eq!(
|
||||||
|
attrs.get("filename"),
|
||||||
|
Some(&"docker/Dockerfile.prod".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_config_with_build_args() {
|
fn frontend_attrs_with_build_args() {
|
||||||
let mut config = minimal_config();
|
let mut config = minimal_config();
|
||||||
config
|
config
|
||||||
.build_args
|
.build_args
|
||||||
@@ -797,67 +772,78 @@ mod tests {
|
|||||||
.build_args
|
.build_args
|
||||||
.insert("BUILD_MODE".to_string(), "release".to_string());
|
.insert("BUILD_MODE".to_string(), "release".to_string());
|
||||||
let step = BuildkitStep::new(config);
|
let step = BuildkitStep::new(config);
|
||||||
let _bc = step.build_config();
|
let attrs = step.build_frontend_attrs();
|
||||||
|
assert_eq!(
|
||||||
|
attrs.get("build-arg:BUILD_MODE"),
|
||||||
|
Some(&"release".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
attrs.get("build-arg:RUST_VERSION"),
|
||||||
|
Some(&"1.78".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// build_exporters tests
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exporters_empty_when_no_tags() {
|
||||||
|
let step = BuildkitStep::new(minimal_config());
|
||||||
|
assert!(step.build_exporters().is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_config_with_cache() {
|
fn exporters_with_tags_and_push() {
|
||||||
|
let mut config = minimal_config();
|
||||||
|
config.tags = vec!["myapp:latest".to_string(), "myapp:v1.0".to_string()];
|
||||||
|
config.push = true;
|
||||||
|
let step = BuildkitStep::new(config);
|
||||||
|
let exporters = step.build_exporters();
|
||||||
|
assert_eq!(exporters.len(), 1);
|
||||||
|
assert_eq!(exporters[0].r#type, "image");
|
||||||
|
assert_eq!(
|
||||||
|
exporters[0].attrs.get("name"),
|
||||||
|
Some(&"myapp:latest,myapp:v1.0".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
exporters[0].attrs.get("push"),
|
||||||
|
Some(&"true".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exporters_with_tags_no_push() {
|
||||||
|
let mut config = minimal_config();
|
||||||
|
config.tags = vec!["myapp:latest".to_string()];
|
||||||
|
config.push = false;
|
||||||
|
let step = BuildkitStep::new(config);
|
||||||
|
let exporters = step.build_exporters();
|
||||||
|
assert_eq!(exporters.len(), 1);
|
||||||
|
assert!(!exporters[0].attrs.contains_key("push"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
// build_cache_options tests
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_options_none_when_empty() {
|
||||||
|
let step = BuildkitStep::new(minimal_config());
|
||||||
|
assert!(step.build_cache_options().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cache_options_with_imports_and_exports() {
|
||||||
let mut config = minimal_config();
|
let mut config = minimal_config();
|
||||||
config.cache_from = vec!["type=registry,ref=myapp:cache".to_string()];
|
config.cache_from = vec!["type=registry,ref=myapp:cache".to_string()];
|
||||||
config.cache_to = vec!["type=registry,ref=myapp:cache,mode=max".to_string()];
|
config.cache_to = vec!["type=registry,ref=myapp:cache,mode=max".to_string()];
|
||||||
let step = BuildkitStep::new(config);
|
let step = BuildkitStep::new(config);
|
||||||
let _bc = step.build_config();
|
let opts = step.build_cache_options().unwrap();
|
||||||
}
|
assert_eq!(opts.imports.len(), 1);
|
||||||
|
assert_eq!(opts.exports.len(), 1);
|
||||||
#[test]
|
assert_eq!(opts.imports[0].r#type, "registry");
|
||||||
fn build_config_with_registry_auth() {
|
assert_eq!(opts.exports[0].r#type, "registry");
|
||||||
let mut config = minimal_config();
|
|
||||||
config.registry_auth.insert(
|
|
||||||
"ghcr.io".to_string(),
|
|
||||||
RegistryAuth {
|
|
||||||
username: "user".to_string(),
|
|
||||||
password: "token".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let step = BuildkitStep::new(config);
|
|
||||||
let _bc = step.build_config();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_config_with_custom_dockerfile() {
|
|
||||||
let mut config = minimal_config();
|
|
||||||
config.dockerfile = "docker/Dockerfile.prod".to_string();
|
|
||||||
let step = BuildkitStep::new(config);
|
|
||||||
let _bc = step.build_config();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_config_all_options_combined() {
|
|
||||||
let mut config = minimal_config();
|
|
||||||
config.buildkit_addr = "tcp://remote:9999".to_string();
|
|
||||||
config.dockerfile = "ci/Dockerfile.ci".to_string();
|
|
||||||
config.context = "/workspace".to_string();
|
|
||||||
config.target = Some("final".to_string());
|
|
||||||
config.tags = vec!["img:v1".to_string()];
|
|
||||||
config.push = true;
|
|
||||||
config.build_args.insert("A".to_string(), "1".to_string());
|
|
||||||
config.cache_from = vec!["type=local,src=/c".to_string()];
|
|
||||||
config.cache_to = vec!["type=local,dest=/c".to_string()];
|
|
||||||
config.tls = TlsConfig {
|
|
||||||
ca: Some("ca".to_string()),
|
|
||||||
cert: Some("cert".to_string()),
|
|
||||||
key: Some("key".to_string()),
|
|
||||||
};
|
|
||||||
config.registry_auth.insert(
|
|
||||||
"ghcr.io".to_string(),
|
|
||||||
RegistryAuth {
|
|
||||||
username: "user".to_string(),
|
|
||||||
password: "pass".to_string(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let step = BuildkitStep::new(config);
|
|
||||||
let _bc = step.build_config();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user