1717//! Operations that list or modify artifacts or the configuration are called by
1818//! Nexus and handled by the Sled Agent API.
1919
20+ use std:: collections:: BTreeMap ;
2021use std:: future:: Future ;
2122use std:: io:: { ErrorKind , Write } ;
2223use std:: net:: SocketAddrV6 ;
@@ -48,6 +49,8 @@ use tokio::sync::{mpsc, oneshot, watch};
4849use tokio:: task:: JoinSet ;
4950use tufaceous_artifact:: ArtifactHash ;
5051
52+ use crate :: services:: ServiceManager ;
53+
5154// These paths are defined under the artifact storage dataset. They
5255// cannot conflict with any artifact paths because all artifact paths are
5356// hexadecimal-encoded SHA-256 checksums.
@@ -87,7 +90,11 @@ pub(crate) struct ArtifactStore<T: DatasetsManager> {
8790}
8891
8992impl < T : DatasetsManager > ArtifactStore < T > {
90- pub ( crate ) async fn new ( log : & Logger , storage : T ) -> ArtifactStore < T > {
93+ pub ( crate ) async fn new (
94+ log : & Logger ,
95+ storage : T ,
96+ services : Option < ServiceManager > ,
97+ ) -> ArtifactStore < T > {
9198 let log = log. new ( slog:: o!( "component" => "ArtifactStore" ) ) ;
9299
93100 let mut ledger_paths = Vec :: new ( ) ;
@@ -126,6 +133,7 @@ impl<T: DatasetsManager> ArtifactStore<T> {
126133 tokio:: task:: spawn ( ledger_manager (
127134 log. clone ( ) ,
128135 ledger_paths,
136+ services,
129137 ledger_rx,
130138 config_tx,
131139 ) ) ;
@@ -244,15 +252,27 @@ impl<T: DatasetsManager> ArtifactStore<T> {
244252 pub ( crate ) async fn get (
245253 & self ,
246254 sha256 : ArtifactHash ,
255+ ) -> Result < File , Error > {
256+ Self :: get_from_storage ( & self . storage , & self . log , sha256) . await
257+ }
258+
259+ /// Open an artifact file by hash from a storage handle.
260+ ///
261+ /// This is the same as [ArtifactStore::get], but can be called with only
262+ /// a [StorageHandle].
263+ pub ( crate ) async fn get_from_storage (
264+ storage : & T ,
265+ log : & Logger ,
266+ sha256 : ArtifactHash ,
247267 ) -> Result < File , Error > {
248268 let sha256_str = sha256. to_string ( ) ;
249269 let mut last_error = None ;
250- for mountpoint in self . storage . artifact_storage_paths ( ) . await {
270+ for mountpoint in storage. artifact_storage_paths ( ) . await {
251271 let path = mountpoint. join ( & sha256_str) ;
252272 match File :: open ( & path) . await {
253273 Ok ( file) => {
254274 info ! (
255- & self . log,
275+ & log,
256276 "Retrieved artifact" ;
257277 "sha256" => & sha256_str,
258278 "path" => path. as_str( ) ,
@@ -261,7 +281,7 @@ impl<T: DatasetsManager> ArtifactStore<T> {
261281 }
262282 Err ( err) if err. kind ( ) == ErrorKind :: NotFound => { }
263283 Err ( err) => {
264- log_and_store ! ( last_error, & self . log, "open" , path, err) ;
284+ log_and_store ! ( last_error, & log, "open" , path, err) ;
265285 }
266286 }
267287 }
@@ -430,9 +450,11 @@ type LedgerManagerRequest =
430450async fn ledger_manager (
431451 log : Logger ,
432452 ledger_paths : Vec < Utf8PathBuf > ,
453+ services : Option < ServiceManager > ,
433454 mut rx : mpsc:: Receiver < LedgerManagerRequest > ,
434455 config_channel : watch:: Sender < Option < ArtifactConfig > > ,
435456) {
457+ let services = services. as_ref ( ) ;
436458 let handle_request = async |new_config : ArtifactConfig | {
437459 if ledger_paths. is_empty ( ) {
438460 return Err ( Error :: NoUpdateDataset ) ;
@@ -441,7 +463,25 @@ async fn ledger_manager(
441463 Ledger :: < ArtifactConfig > :: new ( & log, ledger_paths. clone ( ) ) . await
442464 {
443465 if new_config. generation > ledger. data ( ) . generation {
444- // New config generation; update the ledger.
466+ // New config generation. First check that the configuration
467+ // contains all artifacts that are presently in use.
468+ let mut missing = BTreeMap :: new ( ) ;
469+ // Check artifacts from the current zone configuration.
470+ if let Some ( services) = services {
471+ for zone in services. omicron_zones_list ( ) . await . zones {
472+ if let Some ( hash) = zone. image_source . artifact_hash ( ) {
473+ if !new_config. artifacts . contains ( & hash) {
474+ missing
475+ . insert ( hash, "current zone configuration" ) ;
476+ }
477+ }
478+ }
479+ }
480+ if !missing. is_empty ( ) {
481+ return Err ( Error :: InUseArtifactsMissing ( missing) ) ;
482+ }
483+
484+ // Everything looks okay; update the ledger.
445485 * ledger. data_mut ( ) = new_config;
446486 ledger
447487 } else if new_config == * ledger. data ( ) {
@@ -740,7 +780,7 @@ impl RepoDepotApi for RepoDepotImpl {
740780}
741781
742782#[ derive( Debug , thiserror:: Error , SlogInlineError ) ]
743- pub ( crate ) enum Error {
783+ pub enum Error {
744784 #[ error( "Error while reading request body" ) ]
745785 Body ( dropshot:: HttpError ) ,
746786
@@ -784,6 +824,9 @@ pub(crate) enum Error {
784824 #[ error( "Digest mismatch: expected {expected}, actual {actual}" ) ]
785825 HashMismatch { expected : ArtifactHash , actual : ArtifactHash } ,
786826
827+ #[ error( "Artifacts in use are not present in new config: {0:?}" ) ]
828+ InUseArtifactsMissing ( BTreeMap < ArtifactHash , & ' static str > ) ,
829+
787830 #[ error( "Blocking task failed" ) ]
788831 Join ( #[ source] tokio:: task:: JoinError ) ,
789832
@@ -813,6 +856,7 @@ impl From<Error> for HttpError {
813856 match err {
814857 // 4xx errors
815858 Error :: HashMismatch { .. }
859+ | Error :: InUseArtifactsMissing { .. }
816860 | Error :: NoConfig
817861 | Error :: NotInConfig { .. } => {
818862 HttpError :: for_bad_request ( None , err. to_string ( ) )
@@ -951,7 +995,7 @@ mod test {
951995
952996 let log = test_setup_log ( "generations" ) ;
953997 let backend = TestBackend :: new ( 2 ) ;
954- let store = ArtifactStore :: new ( & log. log , backend) . await ;
998+ let store = ArtifactStore :: new ( & log. log , backend, None ) . await ;
955999
9561000 // get_config returns None
9571001 assert ! ( store. get_config( ) . is_none( ) ) ;
@@ -1004,7 +1048,7 @@ mod test {
10041048 async fn list_get_put ( ) {
10051049 let log = test_setup_log ( "list_get_put" ) ;
10061050 let backend = TestBackend :: new ( 2 ) ;
1007- let mut store = ArtifactStore :: new ( & log. log , backend) . await ;
1051+ let mut store = ArtifactStore :: new ( & log. log , backend, None ) . await ;
10081052
10091053 // get fails, because it doesn't exist yet
10101054 assert ! ( matches!(
@@ -1126,7 +1170,7 @@ mod test {
11261170
11271171 let log = test_setup_log ( "no_dataset" ) ;
11281172 let backend = TestBackend :: new ( 0 ) ;
1129- let store = ArtifactStore :: new ( & log. log , backend) . await ;
1173+ let store = ArtifactStore :: new ( & log. log , backend, None ) . await ;
11301174
11311175 assert ! ( matches!(
11321176 store. get( TEST_HASH ) . await ,
@@ -1154,7 +1198,7 @@ mod test {
11541198
11551199 let log = test_setup_log ( "wrong_hash" ) ;
11561200 let backend = TestBackend :: new ( 2 ) ;
1157- let store = ArtifactStore :: new ( & log. log , backend) . await ;
1201+ let store = ArtifactStore :: new ( & log. log , backend, None ) . await ;
11581202 let mut config = ArtifactConfig {
11591203 generation : 1u32 . into ( ) ,
11601204 artifacts : BTreeSet :: new ( ) ,
@@ -1214,7 +1258,7 @@ mod test {
12141258
12151259 let log = test_setup_log ( "issue_7796" ) ;
12161260 let backend = TestBackend :: new ( 2 ) ;
1217- let store = ArtifactStore :: new ( & log. log , backend) . await ;
1261+ let store = ArtifactStore :: new ( & log. log , backend, None ) . await ;
12181262
12191263 let mut config = ArtifactConfig {
12201264 generation : 1u32 . into ( ) ,
0 commit comments