Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions crates/api-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2254,6 +2254,29 @@ impl Forge for Api {
crate::handlers::machine_validation::get_machine_validation_runs(self, request).await
}

async fn find_machine_validation_run_item_ids(
&self,
request: Request<rpc::MachineValidationRunItemSearchFilter>,
) -> Result<Response<rpc::MachineValidationRunItemIdList>, Status> {
crate::handlers::machine_validation::find_machine_validation_run_item_ids(self, request)
.await
}

async fn find_machine_validation_run_items_by_ids(
&self,
request: Request<rpc::MachineValidationRunItemsByIdsRequest>,
) -> Result<Response<rpc::MachineValidationRunItemList>, Status> {
crate::handlers::machine_validation::find_machine_validation_run_items_by_ids(self, request)
.await
}

async fn get_machine_validation_attempt(
&self,
request: Request<rpc::MachineValidationAttemptGetRequest>,
) -> Result<Response<rpc::MachineValidationAttempt>, Status> {
crate::handlers::machine_validation::get_machine_validation_attempt(self, request).await
}

async fn admin_power_control(
&self,
request: Request<rpc::AdminPowerControlRequest>,
Expand Down
12 changes: 12 additions & 0 deletions crates/api-core/src/auth/internal_rbac_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,18 @@ impl InternalRBACRules {
vec![ForgeAdminCLI, SiteAgent],
);
x.perm("GetMachineValidationRuns", vec![ForgeAdminCLI, SiteAgent]);
x.perm(
"FindMachineValidationRunItemIds",
vec![ForgeAdminCLI, SiteAgent],
);
x.perm(
"FindMachineValidationRunItemsByIds",
vec![ForgeAdminCLI, SiteAgent],
);
x.perm(
"GetMachineValidationAttempt",
vec![ForgeAdminCLI, SiteAgent],
);
x.perm("AdminBmcReset", vec![ForgeAdminCLI]);
x.perm("AdminPowerControl", vec![ForgeAdminCLI, Flow]);
x.perm("DisableSecureBoot", vec![ForgeAdminCLI]);
Expand Down
148 changes: 143 additions & 5 deletions crates/api-core/src/handlers/machine_validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use ::rpc::forge::{self as rpc, GetMachineValidationExternalConfigResponse};
use carbide_machine_controller::config::machine_validation::{
MachineValidationConfig, MachineValidationTestSelectionMode,
};
use carbide_uuid::machine_validation::{MachineValidationAttemptId, MachineValidationRunItemId};
use config_version::ConfigVersion;
use db::{self, machine_validation_suites};
use model::machine::machine_search_config::MachineSearchConfig;
Expand All @@ -27,6 +28,7 @@ use model::machine::{
};
use model::machine_validation::{
MachineValidation, MachineValidationResult, MachineValidationState, MachineValidationStatus,
MachineValidationTest as ModelMachineValidationTest,
MachineValidationTestAddRequest as ModelTestAddRequest,
MachineValidationTestUpdateRequest as ModelTestUpdateRequest,
MachineValidationTestsGetRequest as ModelTestsGetRequest,
Expand Down Expand Up @@ -259,6 +261,21 @@ pub(crate) async fn persist_validation_result(
}
}

// Keep the durable run-item/attempt write ahead of the legacy projections.
// A false return means this report is a replay of an already-terminal attempt.
let first_terminal_report =
db::machine_validation_execution::record_result(&mut txn, &validation_result).await?;
if !first_terminal_report {
tracing::info!(
validation_id = %validation_result.validation_id,
machine_id = %machine.id,
test_id = ?validation_result.test_id,
"machine validation result ignored because attempt was already terminal"
);
txn.commit().await?;
return Ok(tonic::Response::new(()));
}

// Update the Machine validation health report based on the result
let mut updated_validation_health_report = machine.machine_validation_health_report();
updated_validation_health_report.observed_at = Some(chrono::Utc::now());
Expand Down Expand Up @@ -433,6 +450,100 @@ pub(crate) async fn get_machine_validation_runs(
Ok(ret)
}

pub(crate) async fn find_machine_validation_run_item_ids(
api: &Api,
request: tonic::Request<rpc::MachineValidationRunItemSearchFilter>,
) -> Result<tonic::Response<rpc::MachineValidationRunItemIdList>, Status> {
log_request_data(&request);
let req = request.into_inner();
let validation_id = req
.validation_id
.as_ref()
.ok_or(CarbideError::MissingArgument("validation id"))?;

let mut db_reader = api.db_reader();
let run_item_ids = db::machine_validation_execution::find_run_item_ids_by_run_id(
&mut db_reader,
validation_id,
)
.await?
.into_iter()
.map(|id| ::rpc::common::Uuid {
value: id.to_string(),
})
.collect();

Ok(tonic::Response::new(rpc::MachineValidationRunItemIdList {
run_item_ids,
}))
}

pub(crate) async fn find_machine_validation_run_items_by_ids(
api: &Api,
request: tonic::Request<rpc::MachineValidationRunItemsByIdsRequest>,
) -> Result<tonic::Response<rpc::MachineValidationRunItemList>, Status> {
log_request_data(&request);
let req = request.into_inner();

let max_find_by_ids = api.runtime_config.max_find_by_ids as usize;
if req.run_item_ids.len() > max_find_by_ids {
return Err(CarbideError::InvalidArgument(format!(
"no more than {max_find_by_ids} run_item_ids can be accepted"
))
.into());
} else if req.run_item_ids.is_empty() {
return Err(CarbideError::InvalidArgument(
"at least one run_item_id must be provided".to_string(),
)
.into());
}

let run_item_ids = req
.run_item_ids
.iter()
.map(|id| {
uuid::Uuid::try_from(id)
.map(MachineValidationRunItemId::from)
.map_err(CarbideError::from)
})
.collect::<Result<Vec<_>, _>>()?;

let mut db_reader = api.db_reader();
let run_items =
db::machine_validation_execution::find_run_items_by_ids(&mut db_reader, &run_item_ids)
.await?
.into_iter()
.map(rpc::MachineValidationRunItem::from)
.collect();

Ok(tonic::Response::new(rpc::MachineValidationRunItemList {
run_items,
}))
}

pub(crate) async fn get_machine_validation_attempt(
api: &Api,
request: tonic::Request<rpc::MachineValidationAttemptGetRequest>,
) -> Result<tonic::Response<rpc::MachineValidationAttempt>, Status> {
log_request_data(&request);
let req = request.into_inner();
let attempt_id = req
.attempt_id
.as_ref()
.ok_or(CarbideError::MissingArgument("attempt id"))?;
let attempt_id = MachineValidationAttemptId::from(
uuid::Uuid::try_from(attempt_id).map_err(CarbideError::from)?,
);

let attempt =
db::machine_validation_execution::find_attempt_by_id(&api.database_connection, &attempt_id)
.await?;

Ok(tonic::Response::new(rpc::MachineValidationAttempt::from(
attempt,
)))
}

pub(crate) async fn on_demand_machine_validation(
api: &Api,
request: tonic::Request<rpc::MachineValidationOnDemandRequest>,
Expand Down Expand Up @@ -761,19 +872,46 @@ pub(crate) async fn update_machine_validation_run(

let validation_id = req
.validation_id
.as_ref()
.ok_or(CarbideError::MissingArgument("Validation id"))?;
let selected_tests = req
.selected_tests
.into_iter()
.map(ModelMachineValidationTest::try_from)
.collect::<Result<Vec<_>, _>>()?;
let total = req
.total
.try_into()
.map_err(|_e| CarbideError::InvalidArgument("total".to_string()))?;
let total_len =
usize::try_from(total).map_err(|_e| CarbideError::InvalidArgument("total".to_string()))?;

if !selected_tests.is_empty() && total_len != selected_tests.len() {
return Err(CarbideError::InvalidArgument(
"total must match selected_tests length".to_string(),
)
.into());
}

db::machine_validation::update_run(
&mut txn,
validation_id,
req.total
.try_into()
.map_err(|_e| CarbideError::InvalidArgument("total".to_string()))?,
&validation_id,
total,
req.duration_to_complete.unwrap_or_default().seconds,
)
.await?;

if !selected_tests.is_empty() {
let machine_validation =
db::machine_validation::find_by_id(&mut txn, &validation_id).await?;
db::machine_validation_execution::materialize_run_plan(
&mut txn,
&validation_id,
machine_validation.context.as_deref().unwrap_or_default(),
&selected_tests,
)
.await?;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

txn.commit().await?;

Ok(tonic::Response::new(rpc::MachineValidationRunResponse {
Expand Down
2 changes: 2 additions & 0 deletions crates/api-core/src/machine_validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ async fn reconcile_stale_validation(
stale_run_timeout: std::time::Duration,
now: chrono::DateTime<chrono::Utc>,
) -> CarbideResult<bool> {
// Returns true only when this call actually transitions an active stale run.
// False means another path already completed or reconciled the run.
let error_message = format!(
"Machine validation run {} exceeded its expected duration plus stale timeout",
validation.id
Expand Down
1 change: 1 addition & 0 deletions crates/api-core/src/tests/common/api_fixtures/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2736,6 +2736,7 @@ pub async fn update_machine_validation_run(
validation_id,
duration_to_complete,
total,
selected_tests: Vec::new(),
}))
.await
.unwrap()
Expand Down
Loading
Loading