brokkr-agent::work_orders::build Rust
Build handler for Shipwright Build integration.
This module handles the execution of build work orders using Shipwright:
- Parsing Build and WorkOrder resources from YAML
- Applying Build resources to the cluster
- Creating BuildRun resources
- Watching BuildRun status until completion
- Extracting results (image digest, errors)
Structs
brokkr-agent::work_orders::build::BuildRunStatus
pub(crate)
Derives: Debug, Deserialize
BuildRun status for watching completion
Fields
| Name | Type | Description |
|---|---|---|
conditions | Vec < Condition > | |
output | Option < BuildRunOutput > | |
failure_details | Option < FailureDetails > |
brokkr-agent::work_orders::build::Condition
pub(crate)
Derives: Debug, Deserialize
Fields
| Name | Type | Description |
|---|---|---|
condition_type | String | |
status | String | |
reason | Option < String > | |
message | Option < String > |
brokkr-agent::work_orders::build::BuildRunOutput
pub(crate)
Derives: Debug, Deserialize
Fields
| Name | Type | Description |
|---|---|---|
digest | Option < String > | |
size | Option < i64 > |
brokkr-agent::work_orders::build::FailureDetails
pub(crate)
Derives: Debug, Deserialize
Fields
| Name | Type | Description |
|---|---|---|
reason | Option < String > | |
message | Option < String > |
brokkr-agent::work_orders::build::ParsedBuildInfo
pub(crate)
Derives: Debug, Clone, PartialEq
Result of parsing build YAML content
Fields
| Name | Type | Description |
|---|---|---|
build_name | String | |
build_namespace | String | |
build_docs | Vec < serde_yaml :: Value > |
Functions
brokkr-agent::work_orders::build::execute_build
pub
#![allow(unused)]
fn main() {
async fn execute_build (k8s_client : & K8sClient , yaml_content : & str , work_order_id : & str ,) -> Result < Option < String > , Box < dyn std :: error :: Error > >
}
Executes a build using Shipwright.
This function:
- Parses the YAML content to find Build resources
- Applies Build resources to the cluster
- Creates a BuildRun
- Watches the BuildRun until completion
- Returns the image digest on success or error details on failure
Parameters:
| Name | Type | Description |
|---|---|---|
k8s_client | - | Kubernetes client |
yaml_content | - | Multi-document YAML containing Build and WorkOrder |
work_order_id | - | Work order ID for labeling resources |
Returns:
Optional result message (image digest on success)
Source
#![allow(unused)]
fn main() {
pub async fn execute_build(
k8s_client: &K8sClient,
yaml_content: &str,
work_order_id: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
info!("Starting build execution for work order {}", work_order_id);
// Parse YAML documents
let docs: Vec<serde_yaml::Value> = serde_yaml::Deserializer::from_str(yaml_content)
.map(serde_yaml::Value::deserialize)
.collect::<Result<Vec<_>, _>>()?;
if docs.is_empty() {
return Err("No YAML documents found in work order content".into());
}
// Find Build resources and apply them
let mut build_name: Option<String> = None;
let mut build_namespace = String::from("default");
for doc in &docs {
let api_version = doc.get("apiVersion").and_then(|v| v.as_str());
let kind = doc.get("kind").and_then(|v| v.as_str());
match (api_version, kind) {
(Some(av), Some("Build")) if av.starts_with(SHIPWRIGHT_API_GROUP) => {
// Apply the Build resource
let metadata = doc.get("metadata");
if let Some(name) = metadata
.and_then(|m| m.get("name"))
.and_then(|n| n.as_str())
{
build_name = Some(name.to_string());
}
if let Some(ns) = metadata
.and_then(|m| m.get("namespace"))
.and_then(|n| n.as_str())
{
build_namespace = ns.to_string();
}
info!("Applying Shipwright Build resource");
apply_shipwright_resource(k8s_client, doc).await?;
}
(Some(av), Some("WorkOrder")) if av.starts_with("brokkr.io") => {
// Extract buildRef from WorkOrder if present
if let Some(spec) = doc.get("spec") {
if let Some(build_ref) = spec
.get("buildRef")
.and_then(|b| b.get("name"))
.and_then(|n| n.as_str())
{
if build_name.is_none() {
build_name = Some(build_ref.to_string());
}
}
}
debug!("Found brokkr WorkOrder resource, skipping apply (handled separately)");
}
_ => {
debug!("Skipping non-build resource: {:?}/{:?}", api_version, kind);
}
}
}
let build_name = build_name.ok_or("No Build resource or buildRef found in YAML content")?;
info!(
"Using Build '{}' in namespace '{}'",
build_name, build_namespace
);
// Create BuildRun
let buildrun_name = format!(
"{}-{}",
build_name,
&work_order_id[..8.min(work_order_id.len())]
);
info!("Creating BuildRun '{}'", buildrun_name);
let _buildrun = create_buildrun(
k8s_client,
&buildrun_name,
&build_name,
&build_namespace,
work_order_id,
)
.await?;
info!(
"BuildRun '{}' created, waiting for completion",
buildrun_name
);
// Watch BuildRun until completion
let result = watch_buildrun_completion(k8s_client, &buildrun_name, &build_namespace).await?;
Ok(result)
}
}
brokkr-agent::work_orders::build::apply_shipwright_resource
private
#![allow(unused)]
fn main() {
async fn apply_shipwright_resource (k8s_client : & K8sClient , resource : & serde_yaml :: Value ,) -> Result < () , Box < dyn std :: error :: Error > >
}
Applies a Shipwright resource (Build) to the cluster using the core k8s apply logic.
Source
#![allow(unused)]
fn main() {
async fn apply_shipwright_resource(
k8s_client: &K8sClient,
resource: &serde_yaml::Value,
) -> Result<(), Box<dyn std::error::Error>> {
// Convert YAML to DynamicObject
let k8s_object: DynamicObject = serde_yaml::from_value(resource.clone())?;
// Use the existing apply_k8s_objects function which has proper retry logic
let patch_params = PatchParams::apply("brokkr-agent").force();
k8s::api::apply_k8s_objects(&[k8s_object], k8s_client.clone(), patch_params).await
}
}
brokkr-agent::work_orders::build::create_buildrun
private
#![allow(unused)]
fn main() {
async fn create_buildrun (k8s_client : & K8sClient , name : & str , build_name : & str , namespace : & str , work_order_id : & str ,) -> Result < DynamicObject , Box < dyn std :: error :: Error > >
}
Creates a BuildRun resource.
Source
#![allow(unused)]
fn main() {
async fn create_buildrun(
k8s_client: &K8sClient,
name: &str,
build_name: &str,
namespace: &str,
work_order_id: &str,
) -> Result<DynamicObject, Box<dyn std::error::Error>> {
// Discover the BuildRun API
let discovery = Discovery::new(k8s_client.clone()).run().await?;
let ar = discovery
.groups()
.flat_map(|g| g.recommended_resources())
.find(|(ar, _)| ar.group == SHIPWRIGHT_API_GROUP && ar.kind == "BuildRun")
.map(|(ar, _)| ar)
.ok_or("Shipwright BuildRun CRD not found in cluster")?;
let api: Api<DynamicObject> = Api::namespaced_with(k8s_client.clone(), namespace, &ar);
let buildrun_data = json!({
"apiVersion": format!("{}/{}", SHIPWRIGHT_API_GROUP, SHIPWRIGHT_API_VERSION),
"kind": "BuildRun",
"metadata": {
"name": name,
"namespace": namespace,
"labels": {
"brokkr.io/work-order-id": work_order_id,
"shipwright.io/build": build_name
}
},
"spec": {
"build": {
"name": build_name
}
}
});
let buildrun: DynamicObject = serde_json::from_value(buildrun_data)?;
let result = api.create(&PostParams::default(), &buildrun).await?;
Ok(result)
}
}
brokkr-agent::work_orders::build::watch_buildrun_completion
private
#![allow(unused)]
fn main() {
async fn watch_buildrun_completion (k8s_client : & K8sClient , name : & str , namespace : & str ,) -> Result < Option < String > , Box < dyn std :: error :: Error > >
}
Watches a BuildRun until it completes (success or failure).
Source
#![allow(unused)]
fn main() {
async fn watch_buildrun_completion(
k8s_client: &K8sClient,
name: &str,
namespace: &str,
) -> Result<Option<String>, Box<dyn std::error::Error>> {
// Discover the BuildRun API
let discovery = Discovery::new(k8s_client.clone()).run().await?;
let ar = discovery
.groups()
.flat_map(|g| g.recommended_resources())
.find(|(ar, _)| ar.group == SHIPWRIGHT_API_GROUP && ar.kind == "BuildRun")
.map(|(ar, _)| ar)
.ok_or("Shipwright BuildRun CRD not found in cluster")?;
let api: Api<DynamicObject> = Api::namespaced_with(k8s_client.clone(), namespace, &ar);
let start_time = std::time::Instant::now();
let timeout = Duration::from_secs(BUILD_TIMEOUT_SECS);
loop {
if start_time.elapsed() > timeout {
return Err(format!(
"BuildRun '{}' timed out after {} seconds",
name, BUILD_TIMEOUT_SECS
)
.into());
}
let buildrun = api.get(name).await?;
// Check status
if let Some(status_value) = buildrun.data.get("status") {
let status: BuildRunStatus = serde_json::from_value(status_value.clone())?;
// Find the Succeeded condition
for condition in &status.conditions {
if condition.condition_type == CONDITION_SUCCEEDED {
match condition.status.as_str() {
"True" => {
// Build succeeded
let digest = status.output.as_ref().and_then(|o| o.digest.clone());
info!(
"BuildRun '{}' completed successfully. Digest: {:?}",
name, digest
);
return Ok(digest);
}
"False" => {
// Build failed
let error_msg = status
.failure_details
.as_ref()
.map(|f| {
format!(
"{}: {}",
f.reason.as_deref().unwrap_or("Unknown"),
f.message.as_deref().unwrap_or("No message")
)
})
.or_else(|| condition.message.clone())
.unwrap_or_else(|| "Build failed".to_string());
error!("BuildRun '{}' failed: {}", name, error_msg);
return Err(error_msg.into());
}
_ => {
// Still running
debug!(
"BuildRun '{}' still in progress: {:?}",
name, condition.reason
);
}
}
}
}
}
// Wait before next check
sleep(Duration::from_secs(STATUS_POLL_INTERVAL_SECS)).await;
}
}
}
brokkr-agent::work_orders::build::parse_build_yaml
pub(crate)
#![allow(unused)]
fn main() {
fn parse_build_yaml (yaml_content : & str ,) -> Result < ParsedBuildInfo , Box < dyn std :: error :: Error > >
}
Parses YAML content to extract Build resource information.
This function finds Shipwright Build resources in the YAML and extracts:
- The build name (from Build metadata or WorkOrder buildRef)
- The namespace (defaulting to “default”)
- The Build documents to be applied
Parameters:
| Name | Type | Description |
|---|---|---|
yaml_content | - | Multi-document YAML string |
Returns:
ParsedBuildInfo with extracted build details
Source
#![allow(unused)]
fn main() {
pub(crate) fn parse_build_yaml(
yaml_content: &str,
) -> Result<ParsedBuildInfo, Box<dyn std::error::Error>> {
let docs: Vec<serde_yaml::Value> = serde_yaml::Deserializer::from_str(yaml_content)
.map(serde_yaml::Value::deserialize)
.collect::<Result<Vec<_>, _>>()?;
if docs.is_empty() {
return Err("No YAML documents found in work order content".into());
}
let mut build_name: Option<String> = None;
let mut build_namespace = String::from("default");
let mut build_docs = Vec::new();
for doc in &docs {
let api_version = doc.get("apiVersion").and_then(|v| v.as_str());
let kind = doc.get("kind").and_then(|v| v.as_str());
match (api_version, kind) {
(Some(av), Some("Build")) if av.starts_with(SHIPWRIGHT_API_GROUP) => {
let metadata = doc.get("metadata");
if let Some(name) = metadata
.and_then(|m| m.get("name"))
.and_then(|n| n.as_str())
{
build_name = Some(name.to_string());
}
if let Some(ns) = metadata
.and_then(|m| m.get("namespace"))
.and_then(|n| n.as_str())
{
build_namespace = ns.to_string();
}
build_docs.push(doc.clone());
}
(Some(av), Some("WorkOrder")) if av.starts_with("brokkr.io") => {
// Extract buildRef from WorkOrder if present
if let Some(spec) = doc.get("spec") {
if let Some(build_ref) = spec
.get("buildRef")
.and_then(|b| b.get("name"))
.and_then(|n| n.as_str())
{
if build_name.is_none() {
build_name = Some(build_ref.to_string());
}
}
}
}
_ => {
// Skip non-build resources
}
}
}
let build_name = build_name.ok_or("No Build resource or buildRef found in YAML content")?;
Ok(ParsedBuildInfo {
build_name,
build_namespace,
build_docs,
})
}
}
brokkr-agent::work_orders::build::interpret_buildrun_status
pub(crate)
#![allow(unused)]
fn main() {
fn interpret_buildrun_status (status : & BuildRunStatus) -> Result < Option < String > , String >
}
Interprets a BuildRun status to determine completion state.
Returns:
Ok(Some(digest))if the build succeeded -Err(message)if the build failed -Ok(None)if the build is still in progress
Source
#![allow(unused)]
fn main() {
pub(crate) fn interpret_buildrun_status(status: &BuildRunStatus) -> Result<Option<String>, String> {
for condition in &status.conditions {
if condition.condition_type == CONDITION_SUCCEEDED {
match condition.status.as_str() {
"True" => {
// Build succeeded - extract digest
let digest = status.output.as_ref().and_then(|o| o.digest.clone());
return Ok(digest);
}
"False" => {
// Build failed - extract error message
let error_msg = status
.failure_details
.as_ref()
.map(|f| {
format!(
"{}: {}",
f.reason.as_deref().unwrap_or("Unknown"),
f.message.as_deref().unwrap_or("No message")
)
})
.or_else(|| condition.message.clone())
.unwrap_or_else(|| "Build failed".to_string());
return Err(error_msg);
}
_ => {
// Still running (Unknown status)
return Ok(None);
}
}
}
}
// No Succeeded condition found yet - still initializing
Ok(None)
}
}