diff --git a/nexus/src/app/background/init.rs b/nexus/src/app/background/init.rs index 8c946d3c5c..c56a26970a 100644 --- a/nexus/src/app/background/init.rs +++ b/nexus/src/app/background/init.rs @@ -94,6 +94,7 @@ use super::tasks::alert_dispatcher::AlertDispatcher; use super::tasks::bfd; use super::tasks::blueprint_execution; use super::tasks::blueprint_load; +use super::tasks::blueprint_load::LoadedTargetBlueprint; use super::tasks::blueprint_planner; use super::tasks::blueprint_rendezvous; use super::tasks::crdb_node_id_collector; @@ -146,8 +147,6 @@ use nexus_config::DnsTasksConfig; use nexus_db_model::DnsGroup; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::DataStore; -use nexus_types::deployment::Blueprint; -use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::PendingMgsUpdates; use nexus_types::fm; use nexus_types::inventory::Collection; @@ -1179,8 +1178,7 @@ pub struct BackgroundTasksData { /// Channel for TUF repository artifacts to be replicated out to sleds pub tuf_artifact_replication_rx: mpsc::Receiver, /// Channel for exposing the latest loaded blueprint - pub blueprint_load_tx: - watch::Sender)>>, + pub blueprint_load_tx: watch::Sender>, /// `reqwest::Client` for webhook delivery requests. /// /// This is shared with the external API as it's also used when sending diff --git a/nexus/src/app/background/mod.rs b/nexus/src/app/background/mod.rs index e9af8e6026..d60ab87d1b 100644 --- a/nexus/src/app/background/mod.rs +++ b/nexus/src/app/background/mod.rs @@ -138,6 +138,7 @@ pub use init::BackgroundTasksData; pub use init::BackgroundTasksInitializer; pub(crate) use init::BackgroundTasksInternal; pub use nexus_background_task_interface::Activator; +pub(crate) use tasks::blueprint_load::LoadedTargetBlueprint; pub use tasks::saga_recovery::SagaRecoveryHelpers; use futures::future::BoxFuture; diff --git a/nexus/src/app/background/tasks/blueprint_execution.rs b/nexus/src/app/background/tasks/blueprint_execution.rs index a6265b021e..790047d2e5 100644 --- a/nexus/src/app/background/tasks/blueprint_execution.rs +++ b/nexus/src/app/background/tasks/blueprint_execution.rs @@ -5,7 +5,7 @@ //! Background task for realizing a plan blueprint use crate::app::{ - background::{Activator, BackgroundTask}, + background::{Activator, BackgroundTask, LoadedTargetBlueprint}, quiesce::NexusQuiesceHandle, }; use futures::FutureExt; @@ -16,9 +16,7 @@ use nexus_db_queries::db::DataStore; use nexus_reconfigurator_execution::{ RealizeBlueprintOutput, RequiredRealizeArgs, }; -use nexus_types::deployment::{ - Blueprint, BlueprintTarget, PendingMgsUpdates, execution::EventBuffer, -}; +use nexus_types::deployment::{PendingMgsUpdates, execution::EventBuffer}; use omicron_uuid_kinds::OmicronZoneUuid; use serde_json::json; use slog_error_chain::InlineErrorChain; @@ -26,12 +24,12 @@ use std::sync::Arc; use tokio::sync::watch; use update_engine::NestedError; -/// Background task that takes a [`Blueprint`] and realizes the change to +/// Background task that takes a `Blueprint` and realizes the change to /// the state of the system based on the `Blueprint`. pub struct BlueprintExecutor { datastore: Arc, resolver: Resolver, - rx_blueprint: watch::Receiver)>>, + rx_blueprint: watch::Receiver>, nexus_id: OmicronZoneUuid, tx: watch::Sender, saga_recovery: Activator, @@ -43,9 +41,7 @@ impl BlueprintExecutor { pub fn new( datastore: Arc, resolver: Resolver, - rx_blueprint: watch::Receiver< - Option<(BlueprintTarget, Arc)>, - >, + rx_blueprint: watch::Receiver>, nexus_id: OmicronZoneUuid, saga_recovery: Activator, mgs_update_tx: watch::Sender, @@ -79,7 +75,7 @@ impl BlueprintExecutor { // on the watch. let update = self.rx_blueprint.borrow_and_update().clone(); - let Some((bp_target, blueprint)) = update else { + let Some(LoadedTargetBlueprint { target, blueprint }) = update else { warn!( &opctx.log, "Blueprint execution: skipped"; "reason" => "no blueprint", @@ -127,11 +123,13 @@ impl BlueprintExecutor { } }; - if !bp_target.enabled { - warn!(&opctx.log, - "Blueprint execution: skipped"; - "reason" => "blueprint disabled", - "target_id" => %blueprint.id); + if !target.enabled { + warn!( + &opctx.log, + "Blueprint execution: skipped"; + "reason" => "blueprint disabled", + "target_id" => %blueprint.id, + ); return json!({ "target_id": blueprint.id.to_string(), "enabled": false, @@ -220,7 +218,9 @@ impl BackgroundTask for BlueprintExecutor { #[cfg(test)] mod test { use super::BlueprintExecutor; - use crate::app::background::{Activator, BackgroundTask}; + use crate::app::background::{ + Activator, BackgroundTask, LoadedTargetBlueprint, + }; use crate::app::quiesce::NexusQuiesceHandle; use httptest::Expectation; use httptest::matchers::{not, request}; @@ -274,7 +274,7 @@ mod test { opctx: &OpContext, blueprint_zones: BTreeMap>, dns_version: Generation, - ) -> (BlueprintTarget, Blueprint) { + ) -> LoadedTargetBlueprint { let id = BlueprintUuid::new_v4(); // Assume all sleds are active with no disks or datasets. let blueprint_sleds = blueprint_zones @@ -310,7 +310,7 @@ mod test { enabled: true, time_made_target: chrono::Utc::now(), }; - let blueprint = Blueprint { + let blueprint = Arc::new(Blueprint { id, sleds: blueprint_sleds, pending_mgs_updates: PendingMgsUpdates::new(), @@ -329,7 +329,7 @@ mod test { creator: "test".to_string(), comment: "test blueprint".to_string(), source: BlueprintSource::Test, - }; + }); datastore .blueprint_insert(opctx, &blueprint) @@ -340,7 +340,7 @@ mod test { .await .expect("set new blueprint as current target"); - (target, blueprint) + LoadedTargetBlueprint { target, blueprint } } #[nexus_test(server = crate::Server)] @@ -450,18 +450,11 @@ mod test { // With a target blueprint having no zones, the task should trivially // complete and report a successful (empty) summary. let generation = Generation::new(); - let blueprint = { - let (target, blueprint) = create_blueprint( - &datastore, - &opctx, - BTreeMap::new(), - generation, - ) - .await; - (target, Arc::new(blueprint)) - }; - let blueprint_id = blueprint.1.id; - blueprint_tx.send(Some(blueprint)).unwrap(); + let loaded = + create_blueprint(&datastore, &opctx, BTreeMap::new(), generation) + .await; + let blueprint_id = loaded.blueprint.id; + blueprint_tx.send(Some(loaded)).unwrap(); let mut value = task.activate(&opctx).await; let event_buffer = extract_event_buffer(&mut value); @@ -515,7 +508,7 @@ mod test { // In-service zones should be deployed. // // TODO: add expunged zones to the test (should not be deployed). - let mut blueprint = create_blueprint( + let mut loaded = create_blueprint( &datastore, &opctx, BTreeMap::from([ @@ -528,7 +521,7 @@ mod test { // Insert records for the zpools backing the datasets in these zones. for (sled_id, config) in - blueprint.1.all_omicron_zones(BlueprintZoneDisposition::any) + loaded.blueprint.all_omicron_zones(BlueprintZoneDisposition::any) { let Some(dataset) = config.zone_type.durable_dataset() else { continue; @@ -547,9 +540,7 @@ mod test { .expect("failed to upsert zpool"); } - blueprint_tx - .send(Some((blueprint.0, Arc::new(blueprint.1.clone())))) - .unwrap(); + blueprint_tx.send(Some(loaded.clone())).unwrap(); // Make sure that requests get made to the sled agent. for s in [&mut s1, &mut s2] { @@ -568,7 +559,7 @@ mod test { assert_eq!( value, json!({ - "target_id": blueprint.1.id.to_string(), + "target_id": loaded.blueprint.id.to_string(), "execution_error": null, "enabled": true, "needs_saga_recovery": false, @@ -589,19 +580,20 @@ mod test { // Now, disable the target and make sure that we _don't_ invoke the sled // agent. It's enough to just not set expectations on // match_put_omicron_config(). - blueprint.1.internal_dns_version = - blueprint.1.internal_dns_version.next(); - blueprint.0.enabled = false; - blueprint_tx - .send(Some((blueprint.0, Arc::new(blueprint.1.clone())))) - .unwrap(); + { + let blueprint = Arc::make_mut(&mut loaded.blueprint); + blueprint.internal_dns_version = + blueprint.internal_dns_version.next(); + } + loaded.target.enabled = false; + blueprint_tx.send(Some(loaded.clone())).unwrap(); let value = task.activate(&opctx).await; println!("when disabled: {:?}", value); assert_eq!( value, json!({ "enabled": false, - "target_id": blueprint.1.id.to_string() + "target_id": loaded.blueprint.id.to_string() }) ); s1.verify_and_clear(); @@ -616,10 +608,8 @@ mod test { // Do it all again, but configure one of the servers to fail so we can // verify the task's returned summary of what happened. - blueprint.0.enabled = true; - blueprint_tx - .send(Some((blueprint.0, Arc::new(blueprint.1.clone())))) - .unwrap(); + loaded.target.enabled = true; + blueprint_tx.send(Some(loaded.clone())).unwrap(); s1.expect( Expectation::matching(match_put_omicron_config()) .respond_with(status_code(204)), diff --git a/nexus/src/app/background/tasks/blueprint_load.rs b/nexus/src/app/background/tasks/blueprint_load.rs index 3fbd3c92c6..93684b2dd9 100644 --- a/nexus/src/app/background/tasks/blueprint_load.rs +++ b/nexus/src/app/background/tasks/blueprint_load.rs @@ -17,24 +17,28 @@ use serde_json::json; use std::sync::Arc; use tokio::sync::watch; +#[derive(Debug, Clone)] +pub struct LoadedTargetBlueprint { + pub target: BlueprintTarget, + pub blueprint: Arc, +} + pub struct TargetBlueprintLoader { datastore: Arc, - last: Option<(BlueprintTarget, Arc)>, - tx: watch::Sender)>>, + last: Option, + tx: watch::Sender>, } impl TargetBlueprintLoader { pub fn new( datastore: Arc, - tx: watch::Sender)>>, + tx: watch::Sender>, ) -> TargetBlueprintLoader { TargetBlueprintLoader { datastore, last: None, tx } } /// Expose the target blueprint - pub fn watcher( - &self, - ) -> watch::Receiver)>> { + pub fn watcher(&self) -> watch::Receiver> { self.tx.subscribe() } } @@ -49,10 +53,13 @@ impl BackgroundTask for TargetBlueprintLoader { // the current target. let log = match &self.last { None => opctx.log.clone(), - Some(old) => opctx.log.new(o!( - "original_target_id" => old.1.id.to_string(), - "original_time_created" => old.1.time_created.to_string(), - )), + Some(LoadedTargetBlueprint { blueprint, .. }) => { + opctx.log.new(o!( + "original_target_id" => blueprint.id.to_string(), + "original_time_created" => + blueprint.time_created.to_string(), + )) + } }; // Retrieve the latest target blueprint @@ -81,7 +88,10 @@ impl BackgroundTask for TargetBlueprintLoader { // Decide what to do with the new blueprint let enabled = new_bp_target.enabled; - let Some((old_bp_target, old_blueprint)) = self.last.as_ref() + let Some(LoadedTargetBlueprint { + target: old_bp_target, + blueprint: old_blueprint, + }) = self.last.as_ref() else { // We've found a target blueprint for the first time. // Save it and notify any watchers. @@ -93,7 +103,10 @@ impl BackgroundTask for TargetBlueprintLoader { "target_id" => %target_id, "time_created" => %time_created ); - self.last = Some((new_bp_target, Arc::new(new_blueprint))); + self.last = Some(LoadedTargetBlueprint { + target: new_bp_target, + blueprint: Arc::new(new_blueprint), + }); self.tx.send_replace(self.last.clone()); return json!({ "target_id": target_id, @@ -114,7 +127,10 @@ impl BackgroundTask for TargetBlueprintLoader { "target_id" => %target_id, "time_created" => %time_created ); - self.last = Some((new_bp_target, Arc::new(new_blueprint))); + self.last = Some(LoadedTargetBlueprint { + target: new_bp_target, + blueprint: Arc::new(new_blueprint), + }); self.tx.send_replace(self.last.clone()); json!({ "target_id": target_id, @@ -157,7 +173,10 @@ impl BackgroundTask for TargetBlueprintLoader { "time_created" => %time_created, "state" => status, ); - self.last = Some((new_bp_target, Arc::new(new_blueprint))); + self.last = Some(LoadedTargetBlueprint { + target: new_bp_target, + blueprint: Arc::new(new_blueprint), + }); self.tx.send_replace(self.last.clone()); json!({ "target_id": target_id, @@ -268,7 +287,7 @@ mod test { let initial_blueprint = rx.borrow_and_update().clone().expect("no initial blueprint"); let update = serde_json::from_value::(value).unwrap(); - assert_eq!(update.target_id, initial_blueprint.1.id); + assert_eq!(update.target_id, initial_blueprint.blueprint.id); assert_eq!(update.status, "first target blueprint"); let (target, blueprint) = create_blueprint(update.target_id); @@ -278,7 +297,7 @@ mod test { datastore.blueprint_insert(&opctx, &blueprint).await.unwrap(); let value = task.activate(&opctx).await; let update = serde_json::from_value::(value).unwrap(); - assert_eq!(update.target_id, initial_blueprint.1.id); + assert_eq!(update.target_id, initial_blueprint.blueprint.id); assert_eq!(update.status, "target blueprint unchanged"); // Setting a target blueprint makes the loader see it and broadcast it @@ -288,8 +307,8 @@ mod test { assert_eq!(update.target_id, blueprint.id); assert_eq!(update.status, "target blueprint updated"); let rx_update = rx.borrow_and_update().clone().unwrap(); - assert_eq!(rx_update.0, target); - assert_eq!(rx_update.1, blueprint); + assert_eq!(rx_update.target, target); + assert_eq!(rx_update.blueprint, blueprint); // Activation without changing the target blueprint results in no update let value = task.activate(&opctx).await; @@ -310,8 +329,8 @@ mod test { assert_eq!(update.target_id, new_blueprint.id); assert_eq!(update.status, "target blueprint updated"); let rx_update = rx.borrow_and_update().clone().unwrap(); - assert_eq!(rx_update.0, new_target); - assert_eq!(rx_update.1, new_blueprint); + assert_eq!(rx_update.target, new_target); + assert_eq!(rx_update.blueprint, new_blueprint); // Activating again without changing the target blueprint results in // no update diff --git a/nexus/src/app/background/tasks/blueprint_planner.rs b/nexus/src/app/background/tasks/blueprint_planner.rs index 215663d5d8..f9ecba7ab5 100644 --- a/nexus/src/app/background/tasks/blueprint_planner.rs +++ b/nexus/src/app/background/tasks/blueprint_planner.rs @@ -6,6 +6,7 @@ use super::reconfigurator_config::ReconfiguratorConfigLoaderState; use crate::app::background::BackgroundTask; +use crate::app::background::tasks::blueprint_load::LoadedTargetBlueprint; use chrono::Utc; use futures::future::BoxFuture; use nexus_auth::authz; @@ -16,8 +17,8 @@ use nexus_reconfigurator_planning::planner::Planner; use nexus_reconfigurator_planning::planner::PlannerRng; use nexus_reconfigurator_preparation::PlanningInputFromDb; use nexus_types::deployment::BlueprintSource; +use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::PlanningReport; -use nexus_types::deployment::{Blueprint, BlueprintTarget}; use nexus_types::internal_api::background::BlueprintPlannerStatus; use nexus_types::inventory::Collection; use omicron_common::api::external::Error; @@ -58,8 +59,8 @@ pub struct BlueprintPlanner { datastore: Arc, rx_config: Receiver, rx_inventory: Receiver>>, - rx_blueprint: Receiver)>>, - tx_blueprint: Sender)>>, + rx_blueprint: Receiver>, + tx_planned: Sender>, blueprint_limit: u64, } @@ -84,23 +85,37 @@ impl BlueprintPlanner { datastore: Arc, rx_config: Receiver, rx_inventory: Receiver>>, - rx_blueprint: Receiver)>>, + rx_blueprint: Receiver>, ) -> Self { - let (tx_blueprint, _) = watch::channel(None); + let (tx_planned, _) = watch::channel(None); Self { datastore, rx_config, rx_inventory, rx_blueprint, - tx_blueprint, + tx_planned, blueprint_limit: DEFAULT_BLUEPRINT_LIMIT, } } - pub fn watcher( - &self, - ) -> watch::Receiver)>> { - self.tx_blueprint.subscribe() + /// Receiving end of a watch channel that holds the most recent blueprint + /// created and set as the current target by this `BlueprintPlanner`. + /// + /// This exact contents of this channel are unlikely to be useful. If you + /// want the current target blueprint, you should use the channel exposed by + /// the `blueprint_loader` task instead. This channel will often be `None` + /// for an extended period of time after Nexus startup (e.g., any time Nexus + /// starts up and there are no planning changes to be made), and even if + /// it's `Some(blueprint_id)`, the stored ID is the last blueprint planned + /// _by this `BlueprintPlanner`_; the current target blueprint may have + /// already been set to something else by other sources (e.g., the + /// `BlueprintPlanner` tasks on other Nexus instances). + /// + /// The primary use of this channel is to be notified when the planner has + /// created a new target blueprint, at which point a concerned party should + /// load the current target. + pub fn watcher(&self) -> watch::Receiver> { + self.tx_planned.subscribe() } /// Run a planning iteration to generate a new blueprint. @@ -160,7 +175,7 @@ impl BlueprintPlanner { // Get the current target blueprint to use as a parent. // Cloned so that we don't block the channel. - let Some((target, parent)) = + let Some(LoadedTargetBlueprint { target, blueprint: parent }) = self.rx_blueprint.borrow_and_update().clone() else { return Err(PlanError::NoTargetBlueprint); @@ -313,7 +328,7 @@ impl BlueprintPlanner { // We have a new target! - self.tx_blueprint.send_replace(Some((target, Arc::new(blueprint)))); + self.tx_planned.send_replace(Some(blueprint.id)); Ok(BlueprintPlannerStatus::Targeted { parent_blueprint_id, blueprint_id, @@ -456,10 +471,11 @@ mod test { TargetBlueprintLoader::new(datastore.clone(), tx_loader); let mut rx_loader = bp_loader.watcher(); bp_loader.activate(&opctx).await; - let (_initial_target, initial_blueprint) = rx_loader + let initial_blueprint = rx_loader .borrow_and_update() .clone() - .expect("no initial blueprint"); + .expect("no initial blueprint") + .blueprint; // Spin up the inventory collector background task. let resolver = internal_dns_resolver::Resolver::new_from_addrs( @@ -526,7 +542,7 @@ mod test { // Load and check the new target blueprint. bp_loader.activate(&opctx).await; - let (mut target, blueprint) = rx_loader + let LoadedTargetBlueprint { mut target, blueprint } = rx_loader .borrow_and_update() .clone() .expect("failed to load blueprint"); @@ -560,7 +576,7 @@ mod test { // Ping the loader again so it gets the updated target. bp_loader.activate(&opctx).await; - let (target, blueprint) = rx_loader + let LoadedTargetBlueprint { target, blueprint } = rx_loader .borrow_and_update() .clone() .expect("failed to re-load blueprint"); diff --git a/nexus/src/app/background/tasks/blueprint_rendezvous.rs b/nexus/src/app/background/tasks/blueprint_rendezvous.rs index 628a7ac856..d0a2142c53 100644 --- a/nexus/src/app/background/tasks/blueprint_rendezvous.rs +++ b/nexus/src/app/background/tasks/blueprint_rendezvous.rs @@ -5,36 +5,34 @@ //! Background task for reconciling blueprints and inventory, updating //! Reconfigurator rendezvous tables -use crate::app::background::BackgroundTask; +use crate::app::background::{ + BackgroundTask, tasks::blueprint_load::LoadedTargetBlueprint, +}; use futures::FutureExt; use futures::future::BoxFuture; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::DataStore; use nexus_reconfigurator_rendezvous::reconcile_blueprint_rendezvous_tables; use nexus_types::{ - deployment::{Blueprint, BlueprintTarget}, - internal_api::background::BlueprintRendezvousStatus, - inventory::Collection, + internal_api::background::BlueprintRendezvousStatus, inventory::Collection, }; use serde_json::json; use std::sync::Arc; use tokio::sync::watch; -/// Background task that takes a [`Blueprint`] and an inventory `Collection` +/// Background task that takes a `Blueprint` and an inventory `Collection` /// and updates any rendezvous tables to track resources under Reconfigurator's /// control for other parts of Nexus to consume. pub struct BlueprintRendezvous { datastore: Arc, - rx_blueprint: watch::Receiver)>>, + rx_blueprint: watch::Receiver>, rx_inventory: watch::Receiver>>, } impl BlueprintRendezvous { pub fn new( datastore: Arc, - rx_blueprint: watch::Receiver< - Option<(BlueprintTarget, Arc)>, - >, + rx_blueprint: watch::Receiver>, rx_inventory: watch::Receiver>>, ) -> Self { Self { datastore, rx_blueprint, rx_inventory } @@ -50,7 +48,8 @@ impl BlueprintRendezvous { // Get the latest blueprint, cloning to prevent holding a read lock // on the watch. let update = self.rx_blueprint.borrow_and_update().clone(); - let Some((_, blueprint)) = update else { + let Some(LoadedTargetBlueprint { blueprint, target: _ }) = update + else { warn!( &opctx.log, "Blueprint rendezvous: skipped"; "reason" => "no blueprint", diff --git a/nexus/src/app/background/tasks/crdb_node_id_collector.rs b/nexus/src/app/background/tasks/crdb_node_id_collector.rs index 1f8d87cfa6..529c38b7d0 100644 --- a/nexus/src/app/background/tasks/crdb_node_id_collector.rs +++ b/nexus/src/app/background/tasks/crdb_node_id_collector.rs @@ -24,6 +24,7 @@ //! whether a zone without a known node ID ever existed. use crate::app::background::BackgroundTask; +use crate::app::background::tasks::blueprint_load::LoadedTargetBlueprint; use anyhow::Context; use anyhow::ensure; use futures::FutureExt; @@ -33,7 +34,6 @@ use futures::stream; use nexus_auth::context::OpContext; use nexus_db_queries::db::DataStore; use nexus_types::deployment::Blueprint; -use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneType; use nexus_types::deployment::blueprint_zone_type; @@ -46,15 +46,13 @@ use tokio::sync::watch; pub struct CockroachNodeIdCollector { datastore: Arc, - rx_blueprint: watch::Receiver)>>, + rx_blueprint: watch::Receiver>, } impl CockroachNodeIdCollector { pub fn new( datastore: Arc, - rx_blueprint: watch::Receiver< - Option<(BlueprintTarget, Arc)>, - >, + rx_blueprint: watch::Receiver>, ) -> Self { Self { datastore, rx_blueprint } } @@ -74,7 +72,8 @@ impl CockroachNodeIdCollector { // on the watch. let update = self.rx_blueprint.borrow_and_update().clone(); - let Some((_bp_target, blueprint)) = update else { + let Some(LoadedTargetBlueprint { blueprint, target: _ }) = update + else { warn!( &opctx.log, "Blueprint execution: skipped"; "reason" => "no blueprint", @@ -243,6 +242,7 @@ mod tests { use nexus_reconfigurator_planning::example::ExampleSystemBuilder; use nexus_reconfigurator_planning::planner::PlannerRng; use nexus_types::deployment::BlueprintSource; + use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneImageSource; use omicron_common::api::external::Generation; @@ -378,7 +378,10 @@ mod tests { }; let (_tx_blueprint, rx_blueprint) = - watch::channel(Some((blueprint_target, Arc::new(blueprint)))); + watch::channel(Some(LoadedTargetBlueprint { + target: blueprint_target, + blueprint: Arc::new(blueprint), + })); let mut collector = CockroachNodeIdCollector::new(datastore.clone(), rx_blueprint); @@ -438,7 +441,10 @@ mod tests { }; let (_tx_blueprint, rx_blueprint) = - watch::channel(Some((blueprint_target, Arc::new(blueprint)))); + watch::channel(Some(LoadedTargetBlueprint { + target: blueprint_target, + blueprint: Arc::new(blueprint), + })); let mut collector = CockroachNodeIdCollector::new(datastore.clone(), rx_blueprint); diff --git a/nexus/src/app/quiesce.rs b/nexus/src/app/quiesce.rs index 18b1765f15..623f9ab9d3 100644 --- a/nexus/src/app/quiesce.rs +++ b/nexus/src/app/quiesce.rs @@ -4,6 +4,7 @@ //! Manage Nexus quiesce state +use crate::app::background::LoadedTargetBlueprint; use anyhow::{Context, anyhow, bail}; use assert_matches::assert_matches; use chrono::Utc; @@ -11,8 +12,6 @@ use nexus_db_model::DbMetadataNexusState; use nexus_db_queries::authz; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::DataStore; -use nexus_types::deployment::Blueprint; -use nexus_types::deployment::BlueprintTarget; use nexus_types::deployment::BlueprintZoneDisposition; use nexus_types::deployment::BlueprintZoneType; use nexus_types::internal_api::views::QuiesceState; @@ -56,8 +55,7 @@ pub struct NexusQuiesceHandle { my_nexus_id: OmicronZoneUuid, sagas: SagaQuiesceHandle, quiesce_opctx: Arc, - latest_blueprint: - watch::Receiver)>>, + latest_blueprint: watch::Receiver>, state: watch::Sender, } @@ -65,9 +63,7 @@ impl NexusQuiesceHandle { pub fn new( datastore: Arc, my_nexus_id: OmicronZoneUuid, - latest_blueprint: watch::Receiver< - Option<(BlueprintTarget, Arc)>, - >, + latest_blueprint: watch::Receiver>, quiesce_opctx: OpContext, ) -> NexusQuiesceHandle { let saga_quiesce_log = @@ -341,7 +337,7 @@ async fn check_all_sagas_drained( // returns true, and we checked that this is `Some` .unwrap() // extract just the blueprint part - .1 + .blueprint // As usual, we clone to avoid locking the watch channel for the // lifetime of this value. .clone(); @@ -508,6 +504,7 @@ async fn check_all_sagas_drained( #[cfg(test)] mod test { + use crate::app::background::LoadedTargetBlueprint; use crate::app::quiesce::NexusQuiesceHandle; use crate::app::sagas::test_helpers::test_opctx; use assert_matches::assert_matches; @@ -881,8 +878,10 @@ mod test { time_made_target: Utc::now(), }; let blueprint_id = blueprint.id; - let (_, blueprint_rx) = - watch::channel(Some((bp_target, Arc::new(blueprint)))); + let (_, blueprint_rx) = watch::channel(Some(LoadedTargetBlueprint { + target: bp_target, + blueprint: Arc::new(blueprint), + })); // Insert active records for the Nexus instances. let conn = diff --git a/nexus/src/app/update.rs b/nexus/src/app/update.rs index ad711f4971..e0f1e37c9d 100644 --- a/nexus/src/app/update.rs +++ b/nexus/src/app/update.rs @@ -4,6 +4,7 @@ //! Software Updates +use crate::app::background::LoadedTargetBlueprint; use bytes::Bytes; use dropshot::HttpError; use futures::Stream; @@ -15,9 +16,7 @@ use nexus_db_model::TufTrustRoot; use nexus_db_queries::context::OpContext; use nexus_db_queries::db::{datastore::SQL_BATCH_SIZE, pagination::Paginator}; use nexus_types::deployment::SledFilter; -use nexus_types::deployment::{ - Blueprint, BlueprintTarget, TargetReleaseDescription, -}; +use nexus_types::deployment::TargetReleaseDescription; use nexus_types::external_api::shared::TufSignedRootRole; use nexus_types::external_api::views; use nexus_types::identity::Asset; @@ -30,7 +29,6 @@ use omicron_uuid_kinds::{GenericUuid, TufTrustRootUuid}; use semver::Version; use std::collections::BTreeMap; use std::iter; -use std::sync::Arc; use tokio::sync::watch; use update_common::artifacts::{ ArtifactsWithPlan, ControlPlaneZonesMode, VerificationMode, @@ -40,15 +38,12 @@ use uuid::Uuid; /// Used to pull data out of the channels #[derive(Clone)] pub struct UpdateStatusHandle { - latest_blueprint: - watch::Receiver)>>, + latest_blueprint: watch::Receiver>, } impl UpdateStatusHandle { pub fn new( - latest_blueprint: watch::Receiver< - Option<(BlueprintTarget, Arc)>, - >, + latest_blueprint: watch::Receiver>, ) -> Self { Self { latest_blueprint } } @@ -220,7 +215,7 @@ impl super::Nexus { ) .await?; - let (blueprint_target, blueprint) = self + let blueprint_target = self .update_status .latest_blueprint .borrow() @@ -232,12 +227,12 @@ impl super::Nexus { ) })?; - let time_last_step_planned = blueprint_target.time_made_target; + let time_last_step_planned = blueprint_target.target.time_made_target; // Update activity is suspended if the current target release generation // is less than the blueprint's minimum generation let suspended = *db_target_release.generation - < blueprint.target_release_minimum_generation; + < blueprint_target.blueprint.target_release_minimum_generation; Ok(views::UpdateStatus { target_release: Nullable(target_release),