diff --git a/openapi/wicketd.json b/openapi/wicketd.json index a75c965ad8..e0b37f1ba2 100644 --- a/openapi/wicketd.json +++ b/openapi/wicketd.json @@ -131,44 +131,31 @@ } } }, - "/clear-update-state/{type}/{slot}": { + "/clear-update-state": { "post": { "summary": "Resets update state for a sled.", "description": "Use this to clear update state after a failed update.", "operationId": "post_clear_update_state", - "parameters": [ - { - "in": "path", - "name": "slot", - "required": true, - "schema": { - "type": "integer", - "format": "uint32", - "minimum": 0 - } - }, - { - "in": "path", - "name": "type", - "required": true, - "schema": { - "$ref": "#/components/schemas/SpType" - } - } - ], "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ClearUpdateStateOptions" + "$ref": "#/components/schemas/ClearUpdateStateParams" } } }, "required": true }, "responses": { - "204": { - "description": "resource updated" + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearUpdateStateResponse" + } + } + } }, "4XX": { "$ref": "#/components/responses/Error" @@ -1014,6 +1001,56 @@ } } }, + "ClearUpdateStateParams": { + "type": "object", + "properties": { + "options": { + "description": "Options for clearing update state", + "allOf": [ + { + "$ref": "#/components/schemas/ClearUpdateStateOptions" + } + ] + }, + "targets": { + "description": "The SP identifiers to clear the update state for. Must be non-empty.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "uniqueItems": true + } + }, + "required": [ + "options", + "targets" + ] + }, + "ClearUpdateStateResponse": { + "type": "object", + "properties": { + "cleared": { + "description": "The SPs for which update data was cleared.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "uniqueItems": true + }, + "no_update_data": { + "description": "The SPs that had no update state to clear.", + "type": "array", + "items": { + "$ref": "#/components/schemas/SpIdentifier" + }, + "uniqueItems": true + } + }, + "required": [ + "cleared", + "no_update_data" + ] + }, "CurrentRssUserConfig": { "type": "object", "properties": { diff --git a/wicket/src/wicketd.rs b/wicket/src/wicketd.rs index 2411542429..33c80410d8 100644 --- a/wicket/src/wicketd.rs +++ b/wicket/src/wicketd.rs @@ -10,9 +10,10 @@ use std::net::SocketAddrV6; use tokio::sync::mpsc::{self, Sender, UnboundedSender}; use tokio::time::{interval, Duration, MissedTickBehavior}; use wicketd_client::types::{ - AbortUpdateOptions, ClearUpdateStateOptions, GetInventoryParams, - GetInventoryResponse, GetLocationResponse, IgnitionCommand, SpIdentifier, - SpType, StartUpdateOptions, StartUpdateParams, + AbortUpdateOptions, ClearUpdateStateOptions, ClearUpdateStateParams, + GetInventoryParams, GetInventoryResponse, GetLocationResponse, + IgnitionCommand, SpIdentifier, SpType, StartUpdateOptions, + StartUpdateParams, }; use crate::events::EventReportMap; @@ -229,14 +230,15 @@ impl WicketdManager { tokio::spawn(async move { let update_client = create_wicketd_client(&log, addr, WICKETD_TIMEOUT); - let sp: SpIdentifier = component_id.into(); - let response = match update_client - .post_clear_update_state(sp.type_, sp.slot, &options) - .await - { - Ok(_) => Ok(()), - Err(error) => Err(error.to_string()), + let params = ClearUpdateStateParams { + targets: vec![component_id.into()], + options, }; + let response = + match update_client.post_clear_update_state(¶ms).await { + Ok(_) => Ok(()), + Err(error) => Err(error.to_string()), + }; slog::info!( log, diff --git a/wicketd/src/http_entrypoints.rs b/wicketd/src/http_entrypoints.rs index be0f681601..d6cb6ebd6d 100644 --- a/wicketd/src/http_entrypoints.rs +++ b/wicketd/src/http_entrypoints.rs @@ -708,6 +708,15 @@ pub(crate) enum UpdateSimulatedResult { Failure, } +#[derive(Clone, Debug, JsonSchema, Deserialize)] +pub(crate) struct ClearUpdateStateParams { + /// The SP identifiers to clear the update state for. Must be non-empty. + pub(crate) targets: BTreeSet, + + /// Options for clearing update state + pub(crate) options: ClearUpdateStateOptions, +} + #[derive(Clone, Debug, JsonSchema, Deserialize)] pub(crate) struct ClearUpdateStateOptions { /// If passed in, fails the clear update state operation with a simulated @@ -715,6 +724,15 @@ pub(crate) struct ClearUpdateStateOptions { pub(crate) test_error: Option, } +#[derive(Clone, Debug, Default, JsonSchema, Serialize)] +pub(crate) struct ClearUpdateStateResponse { + /// The SPs for which update data was cleared. + pub(crate) cleared: BTreeSet, + + /// The SPs that had no update state to clear. + pub(crate) no_update_data: BTreeSet, +} + #[derive(Clone, Debug, JsonSchema, Deserialize)] pub(crate) struct AbortUpdateOptions { /// The message to abort the update with. @@ -1080,25 +1098,31 @@ async fn post_abort_update( /// Use this to clear update state after a failed update. #[endpoint { method = POST, - path = "/clear-update-state/{type}/{slot}", + path = "/clear-update-state", }] async fn post_clear_update_state( rqctx: RequestContext, - target: Path, - opts: TypedBody, -) -> Result { + params: TypedBody, +) -> Result, HttpError> { let log = &rqctx.log; - let target = target.into_inner(); + let rqctx = rqctx.context(); + let params = params.into_inner(); - let opts = opts.into_inner(); - if let Some(test_error) = opts.test_error { + if params.targets.is_empty() { + return Err(HttpError::for_bad_request( + None, + "No targets specified".into(), + )); + } + + if let Some(test_error) = params.options.test_error { return Err(test_error .into_http_error(log, "clearing update state") .await); } - match rqctx.context().update_tracker.clear_update_state(target).await { - Ok(()) => Ok(HttpResponseUpdatedNoContent {}), + match rqctx.update_tracker.clear_update_state(params.targets).await { + Ok(response) => Ok(HttpResponseOk(response)), Err(err) => Err(err.to_http_error()), } } diff --git a/wicketd/src/update_tracker.rs b/wicketd/src/update_tracker.rs index bd8e187fe9..368579fd55 100644 --- a/wicketd/src/update_tracker.rs +++ b/wicketd/src/update_tracker.rs @@ -8,6 +8,7 @@ use crate::artifacts::ArtifactIdData; use crate::artifacts::UpdatePlan; use crate::artifacts::WicketdArtifactStore; use crate::helpers::sps_to_string; +use crate::http_entrypoints::ClearUpdateStateResponse; use crate::http_entrypoints::GetArtifactsAndEventReportsResponse; use crate::http_entrypoints::StartUpdateOptions; use crate::http_entrypoints::UpdateSimulatedResult; @@ -184,10 +185,10 @@ impl UpdateTracker { pub(crate) async fn clear_update_state( &self, - sp: SpIdentifier, - ) -> Result<(), ClearUpdateStateError> { + sps: BTreeSet, + ) -> Result { let mut update_data = self.sp_update_data.lock().await; - update_data.clear_update_state(sp) + update_data.clear_update_state(&sps) } pub(crate) async fn abort_update( @@ -609,19 +610,38 @@ impl UpdateTrackerData { fn clear_update_state( &mut self, - sp: SpIdentifier, - ) -> Result<(), ClearUpdateStateError> { - // Is an update currently running? If so, then reject the request. - let is_running = self - .sp_update_data - .get(&sp) - .map_or(false, |update_data| !update_data.task.is_finished()); - if is_running { - return Err(ClearUpdateStateError::UpdateInProgress); + sps: &BTreeSet, + ) -> Result { + // Are any updates currently running? If so, then reject the request. + let in_progress_updates = sps + .iter() + .filter_map(|sp| { + self.sp_update_data + .get(sp) + .map_or(false, |update_data| { + !update_data.task.is_finished() + }) + .then(|| *sp) + }) + .collect::>(); + + if !in_progress_updates.is_empty() { + return Err(ClearUpdateStateError::UpdateInProgress( + in_progress_updates, + )); } - self.sp_update_data.remove(&sp); - Ok(()) + let mut resp = ClearUpdateStateResponse::default(); + + for sp in sps { + if self.sp_update_data.remove(sp).is_some() { + resp.cleared.insert(*sp); + } else { + resp.no_update_data.insert(*sp); + } + } + + Ok(resp) } async fn abort_update( @@ -695,8 +715,8 @@ pub enum StartUpdateError { #[derive(Debug, Clone, Error, Eq, PartialEq)] pub enum ClearUpdateStateError { - #[error("target is currently being updated")] - UpdateInProgress, + #[error("targets are currently being updated: {}", sps_to_string(.0))] + UpdateInProgress(Vec), } impl ClearUpdateStateError { @@ -704,7 +724,7 @@ impl ClearUpdateStateError { let message = DisplayErrorChain::new(self).to_string(); match self { - ClearUpdateStateError::UpdateInProgress => { + ClearUpdateStateError::UpdateInProgress(_) => { HttpError::for_bad_request(None, message) } }