@@ -37,11 +37,9 @@ use omicron_common::api::external::Error;
3737use omicron_common:: api:: external:: ListResultVec ;
3838use omicron_common:: api:: external:: LookupResult ;
3939use omicron_common:: api:: external:: LookupType ;
40- use omicron_common:: api:: external:: Name as ExternalName ;
4140use omicron_common:: api:: external:: ResourceType ;
4241use omicron_common:: api:: external:: UpdateResult ;
4342use ref_cast:: RefCast ;
44- use std:: str:: FromStr ;
4543use uuid:: Uuid ;
4644
4745impl DataStore {
@@ -74,18 +72,42 @@ impl DataStore {
7472 . map_err ( |e| public_error_from_diesel_pool ( e, ErrorHandler :: Server ) )
7573 }
7674
77- /// Looks up the default IP pool by name.
75+ /// Looks up the default IP pool for a given scope, i.e., a given
76+ /// combination of silo and project ID (or none). If there is no default at
77+ /// a given scope, fall back up a level. There should always be a default at
78+ /// fleet level, though this query can theoretically fail.
7879 pub async fn ip_pools_fetch_default_for (
7980 & self ,
8081 opctx : & OpContext ,
8182 action : authz:: Action ,
82- ) -> LookupResult < ( authz:: IpPool , IpPool ) > {
83- self . ip_pools_fetch_for (
84- opctx,
85- action,
86- & Name ( ExternalName :: from_str ( "default" ) . unwrap ( ) ) ,
87- )
88- . await
83+ silo_id : Option < Uuid > ,
84+ project_id : Option < Uuid > ,
85+ ) -> LookupResult < IpPool > {
86+ use db:: schema:: ip_pool:: dsl;
87+ opctx. authorize ( action, & authz:: IP_POOL_LIST ) . await ?;
88+
89+ dsl:: ip_pool
90+ . filter ( dsl:: silo_id. eq ( silo_id) . or ( dsl:: silo_id. is_null ( ) ) )
91+ . filter (
92+ dsl:: project_id. eq ( project_id) . or ( dsl:: project_id. is_null ( ) ) ,
93+ )
94+ . filter ( dsl:: default. eq ( true ) )
95+ . filter ( dsl:: time_deleted. is_null ( ) )
96+ // this will sort by most specific first, i.e.,
97+ //
98+ // (silo, project)
99+ // (silo, null)
100+ // (null, null)
101+ //
102+ // then by only taking the first result, we get the most specific one
103+ . order ( (
104+ dsl:: project_id. asc ( ) . nulls_last ( ) ,
105+ dsl:: silo_id. asc ( ) . nulls_last ( ) ,
106+ ) )
107+ . select ( IpPool :: as_select ( ) )
108+ . first_async :: < IpPool > ( self . pool_authorized ( opctx) . await ?)
109+ . await
110+ . map_err ( |e| public_error_from_diesel_pool ( e, ErrorHandler :: Server ) )
89111 }
90112
91113 /// Looks up an IP pool by name.
@@ -429,3 +451,94 @@ impl DataStore {
429451 }
430452 }
431453}
454+
455+ #[ cfg( test) ]
456+ mod test {
457+ use crate :: authz;
458+ use crate :: db:: datastore:: datastore_test;
459+ use crate :: db:: model:: IpPool ;
460+ use nexus_test_utils:: db:: test_setup_database;
461+ use nexus_types:: identity:: Resource ;
462+ use omicron_common:: api:: external:: IdentityMetadataCreateParams ;
463+ use omicron_test_utils:: dev;
464+
465+ #[ tokio:: test]
466+ async fn test_default_ip_pools ( ) {
467+ let logctx = dev:: test_setup_log ( "test_default_ip_pools" ) ;
468+ let mut db = test_setup_database ( & logctx. log ) . await ;
469+ let ( opctx, datastore) = datastore_test ( & logctx, & db) . await ;
470+
471+ let action = authz:: Action :: ListChildren ;
472+
473+ // we start out with the default fleet-level pool already created,
474+ // so when we ask for the fleet default (no silo or project) we get it back
475+ let fleet_default_pool = datastore
476+ . ip_pools_fetch_default_for ( & opctx, action, None , None )
477+ . await
478+ . unwrap ( ) ;
479+
480+ assert_eq ! ( fleet_default_pool. identity. name. as_str( ) , "default" ) ;
481+ assert ! ( fleet_default_pool. default ) ;
482+ assert_eq ! ( fleet_default_pool. silo_id, None ) ;
483+ assert_eq ! ( fleet_default_pool. project_id, None ) ;
484+
485+ // now the interesting thing is that when we fetch the default pool for
486+ // a particular silo or a particular project, if those scopes do not
487+ // have a default IP pool, we will still get back the fleet default
488+
489+ // default for "current" silo is still the fleet default one because it
490+ // has no default of its own
491+ let silo_id = opctx. authn . silo_required ( ) . unwrap ( ) . id ( ) ;
492+ let ip_pool = datastore
493+ . ip_pools_fetch_default_for ( & opctx, action, Some ( silo_id) , None )
494+ . await
495+ . expect ( "Failed to get silo's default IP pool" ) ;
496+ assert_eq ! ( ip_pool. id( ) , fleet_default_pool. id( ) ) ;
497+
498+ // create a non-default pool for the silo
499+ let identity = IdentityMetadataCreateParams {
500+ name : "non-default-for-silo" . parse ( ) . unwrap ( ) ,
501+ description : "" . to_string ( ) ,
502+ } ;
503+ let _ = datastore
504+ . ip_pool_create (
505+ & opctx,
506+ IpPool :: new ( & identity, Some ( silo_id) , /*default= */ false ) ,
507+ )
508+ . await ;
509+
510+ // because that one was not a default, when we ask for silo default
511+ // pool, we still get the fleet default
512+ let ip_pool = datastore
513+ . ip_pools_fetch_default_for ( & opctx, action, Some ( silo_id) , None )
514+ . await
515+ . expect ( "Failed to get fleet default IP pool" ) ;
516+ assert_eq ! ( ip_pool. id( ) , fleet_default_pool. id( ) ) ;
517+
518+ // now create a default pool for the silo
519+ let identity = IdentityMetadataCreateParams {
520+ name : "default-for-silo" . parse ( ) . unwrap ( ) ,
521+ description : "" . to_string ( ) ,
522+ } ;
523+ let _ = datastore
524+ . ip_pool_create ( & opctx, IpPool :: new ( & identity, Some ( silo_id) , true ) )
525+ . await ;
526+
527+ // now when we ask for the silo default pool, we get the one we just made
528+ let ip_pool = datastore
529+ . ip_pools_fetch_default_for ( & opctx, action, Some ( silo_id) , None )
530+ . await
531+ . expect ( "Failed to get silo's default IP pool" ) ;
532+ assert_eq ! ( ip_pool. name( ) . as_str( ) , "default-for-silo" ) ;
533+
534+ // and of course, if we ask for the fleet default again we still get that one
535+ let ip_pool = datastore
536+ . ip_pools_fetch_default_for ( & opctx, action, None , None )
537+ . await
538+ . expect ( "Failed to get fleet default IP pool" ) ;
539+ assert_eq ! ( ip_pool. id( ) , fleet_default_pool. id( ) ) ;
540+
541+ db. cleanup ( ) . await . unwrap ( ) ;
542+ logctx. cleanup_successful ( ) ;
543+ }
544+ }
0 commit comments