@@ -98,8 +98,7 @@ const MAX_PORT: i32 = u16::MAX as _;
9898/// FROM
9999/// ip_pool_range
100100/// WHERE
101- /// project_id = <project_id> OR
102- /// project_id IS NULL AND
101+ /// <pool restriction clause> AND
103102/// time_deleted IS NULL
104103/// )
105104/// CROSS JOIN
@@ -183,6 +182,27 @@ const MAX_PORT: i32 = u16::MAX as _;
183182/// (even though we send the start/stop to the database as `i32`s). We need to
184183/// cast it on the way out of the database, so that Diesel can correctly
185184/// deserialize it into an `i32`.
185+ ///
186+ /// Pool restriction
187+ /// ----------------
188+ ///
189+ /// Clients may optionally request an external address from a specific IP Pool.
190+ /// If they don't provide a pool, the query is restricted to all IP Pools
191+ /// available to their project (not reserved for a different project). In the
192+ /// first case, the restriction is:
193+ ///
194+ /// ```sql
195+ /// pool_id = <pool_id>
196+ /// ```
197+ ///
198+ /// In the latter:
199+ ///
200+ /// ```sql
201+ /// (project_id = <project_id> OR project_id IS NULL)
202+ /// ```
203+ ///
204+ /// I.e., the pool is reserved for _this instance's_ project, or not reserved at
205+ /// all.
186206#[ derive( Debug , Clone ) ]
187207pub struct NextExternalIp {
188208 // TODO-completeness: Add `ip_pool_id`, and restrict search to it.
@@ -390,7 +410,7 @@ impl NextExternalIp {
390410 // FROM
391411 // ip_pool_range
392412 // WHERE
393- // (project_id = <project_id> OR project_id IS NULL) AND
413+ // <pool_restriction> AND
394414 // time_deleted IS NULL
395415 // ```
396416 fn push_address_sequence_subquery < ' a > (
@@ -410,13 +430,21 @@ impl NextExternalIp {
410430 out. push_identifier ( dsl:: first_address:: NAME ) ?;
411431 out. push_sql ( ") AS candidate_ip FROM " ) ;
412432 IP_POOL_RANGE_FROM_CLAUSE . walk_ast ( out. reborrow ( ) ) ?;
413- out. push_sql ( " WHERE (" ) ;
414- out. push_identifier ( dsl:: project_id:: NAME ) ?;
415- out. push_sql ( " = " ) ;
416- out. push_bind_param :: < sql_types:: Uuid , Uuid > ( self . ip . project_id ( ) ) ?;
417- out. push_sql ( " OR " ) ;
418- out. push_identifier ( dsl:: project_id:: NAME ) ?;
419- out. push_sql ( " IS NULL) AND " ) ;
433+ out. push_sql ( " WHERE " ) ;
434+ if let Some ( ref pool_id) = self . ip . pool_id ( ) {
435+ out. push_identifier ( dsl:: ip_pool_id:: NAME ) ?;
436+ out. push_sql ( " = " ) ;
437+ out. push_bind_param :: < sql_types:: Uuid , Uuid > ( pool_id) ?;
438+ } else {
439+ out. push_sql ( "(" ) ;
440+ out. push_identifier ( dsl:: project_id:: NAME ) ?;
441+ out. push_sql ( " = " ) ;
442+ out. push_bind_param :: < sql_types:: Uuid , Uuid > ( self . ip . project_id ( ) ) ?;
443+ out. push_sql ( " OR " ) ;
444+ out. push_identifier ( dsl:: project_id:: NAME ) ?;
445+ out. push_sql ( " IS NULL)" ) ;
446+ }
447+ out. push_sql ( " AND " ) ;
420448 out. push_identifier ( dsl:: time_deleted:: NAME ) ?;
421449 out. push_sql ( " IS NULL" ) ;
422450 Ok ( ( ) )
@@ -581,6 +609,7 @@ mod tests {
581609 use crate :: db:: model:: IpKind ;
582610 use crate :: db:: model:: IpPool ;
583611 use crate :: db:: model:: IpPoolRange ;
612+ use crate :: db:: model:: Name ;
584613 use crate :: external_api:: shared:: IpRange ;
585614 use async_bb8_diesel:: AsyncRunQueryDsl ;
586615 use dropshot:: test_util:: LogContext ;
@@ -999,4 +1028,51 @@ mod tests {
9991028
10001029 context. success ( ) . await ;
10011030 }
1031+
1032+ #[ tokio:: test]
1033+ async fn test_next_external_ip_is_restricted_to_pools ( ) {
1034+ let context =
1035+ TestContext :: new ( "test_next_external_ip_is_restricted_to_pools" )
1036+ . await ;
1037+
1038+ // Create two pools, neither project-restricted.
1039+ let first_range = IpRange :: try_from ( (
1040+ Ipv4Addr :: new ( 10 , 0 , 0 , 1 ) ,
1041+ Ipv4Addr :: new ( 10 , 0 , 0 , 3 ) ,
1042+ ) )
1043+ . unwrap ( ) ;
1044+ context. create_ip_pool ( "p0" , first_range, None ) . await ;
1045+ let second_range = IpRange :: try_from ( (
1046+ Ipv4Addr :: new ( 10 , 0 , 0 , 4 ) ,
1047+ Ipv4Addr :: new ( 10 , 0 , 0 , 6 ) ,
1048+ ) )
1049+ . unwrap ( ) ;
1050+ context. create_ip_pool ( "p1" , second_range, None ) . await ;
1051+
1052+ // Allocating an address on an instance in the second pool should be
1053+ // respected, even though there are IPs available in the first.
1054+ let instance_id = Uuid :: new_v4 ( ) ;
1055+ let project_id = Uuid :: new_v4 ( ) ;
1056+ let id = Uuid :: new_v4 ( ) ;
1057+ let pool_name = Some ( Name ( "p1" . parse ( ) . unwrap ( ) ) ) ;
1058+
1059+ let ip = context
1060+ . db_datastore
1061+ . allocate_instance_ephemeral_ip (
1062+ & context. opctx ,
1063+ id,
1064+ project_id,
1065+ instance_id,
1066+ pool_name,
1067+ )
1068+ . await
1069+ . expect ( "Failed to allocate instance ephemeral IP address" ) ;
1070+ assert_eq ! ( ip. kind, IpKind :: Ephemeral ) ;
1071+ assert_eq ! ( ip. ip. ip( ) , second_range. first_address( ) ) ;
1072+ assert_eq ! ( ip. first_port. 0 , 0 ) ;
1073+ assert_eq ! ( ip. last_port. 0 , u16 :: MAX ) ;
1074+ assert_eq ! ( ip. project_id, project_id) ;
1075+
1076+ context. success ( ) . await ;
1077+ }
10021078}
0 commit comments