@@ -406,9 +406,14 @@ impl DataStore {
406406 ) -> ListResultVec < external:: AntiAffinityGroupMember > {
407407 opctx. authorize ( authz:: Action :: Read , authz_anti_affinity_group) . await ?;
408408
409+ let asc = match pagparams. direction {
410+ dropshot:: PaginationOrder :: Ascending => true ,
411+ dropshot:: PaginationOrder :: Descending => false ,
412+ } ;
413+
409414 let mut query = QueryBuilder :: new ( )
410415 . sql (
411- "
416+ "SELECT id,label FROM (
412417 SELECT instance_id as id, 'instance' as label
413418 FROM anti_affinity_group_instance_membership
414419 WHERE group_id = " ,
@@ -424,24 +429,23 @@ impl DataStore {
424429 )
425430 . param ( )
426431 . bind :: < diesel:: sql_types:: Uuid , _ > ( authz_anti_affinity_group. id ( ) )
427- . sql ( " " ) ;
428-
429- let ( sort, cmp) = match pagparams. direction {
430- dropshot:: PaginationOrder :: Ascending => ( " ORDER BY id ASC " , ">" ) ,
431- dropshot:: PaginationOrder :: Descending => {
432- ( " ORDER BY id DESC " , "<" )
433- }
434- } ;
432+ . sql ( ") " ) ;
435433 if let Some ( id) = pagparams. marker {
436434 query = query
437435 . sql ( "WHERE id " )
438- . sql ( cmp )
436+ . sql ( if asc { ">" } else { "<" } )
439437 . sql ( " " )
440438 . param ( )
441439 . bind :: < diesel:: sql_types:: Uuid , _ > ( * id) ;
442440 } ;
443441
444- query = query. sql ( sort) ;
442+ query = query. sql ( " ORDER BY id " ) ;
443+ if asc {
444+ query = query. sql ( "ASC " ) ;
445+ } else {
446+ query = query. sql ( "DESC " ) ;
447+ }
448+
445449 query =
446450 query. sql ( " LIMIT " ) . param ( ) . bind :: < diesel:: sql_types:: BigInt , _ > (
447451 i64:: from ( pagparams. limit . get ( ) ) ,
@@ -1230,6 +1234,7 @@ mod tests {
12301234 use nexus_types:: external_api:: params;
12311235 use omicron_common:: api:: external:: {
12321236 self , ByteCount , DataPageParams , IdentityMetadataCreateParams ,
1237+ SimpleIdentity ,
12331238 } ;
12341239 use omicron_test_utils:: dev;
12351240 use omicron_uuid_kinds:: GenericUuid ;
@@ -1890,6 +1895,172 @@ mod tests {
18901895 logctx. cleanup_successful ( ) ;
18911896 }
18921897
1898+ // Anti-affinity group member listing has a slightly more complicated
1899+ // implementation, because it queries multiple tables and UNIONs them
1900+ // together.
1901+ //
1902+ // This test exists to validate that manual implementation.
1903+ #[ tokio:: test]
1904+ async fn anti_affinity_group_membership_list_extended ( ) {
1905+ // Setup
1906+ let logctx =
1907+ dev:: test_setup_log ( "anti_affinity_group_membership_list_extended" ) ;
1908+ let db = TestDatabase :: new_with_datastore ( & logctx. log ) . await ;
1909+ let ( opctx, datastore) = ( db. opctx ( ) , db. datastore ( ) ) ;
1910+
1911+ // Create a project and a group
1912+ let ( authz_project, ..) =
1913+ create_project ( & opctx, & datastore, "my-project" ) . await ;
1914+ let group = create_anti_affinity_group (
1915+ & opctx,
1916+ & datastore,
1917+ & authz_project,
1918+ "my-group" ,
1919+ )
1920+ . await
1921+ . unwrap ( ) ;
1922+
1923+ let ( .., authz_aa_group) = LookupPath :: new ( opctx, datastore)
1924+ . anti_affinity_group_id ( group. id ( ) )
1925+ . lookup_for ( authz:: Action :: Modify )
1926+ . await
1927+ . unwrap ( ) ;
1928+
1929+ // A new group should have no members
1930+ let pagparams = DataPageParams {
1931+ marker : None ,
1932+ limit : NonZeroU32 :: new ( 100 ) . unwrap ( ) ,
1933+ direction : dropshot:: PaginationOrder :: Ascending ,
1934+ } ;
1935+ let members = datastore
1936+ . anti_affinity_group_member_list (
1937+ & opctx,
1938+ & authz_aa_group,
1939+ & pagparams,
1940+ )
1941+ . await
1942+ . unwrap ( ) ;
1943+ assert ! ( members. is_empty( ) ) ;
1944+
1945+ // Add some groups and instances, so we have data to list over.
1946+
1947+ const INSTANCE_COUNT : usize = 3 ;
1948+ const AFFINITY_GROUP_COUNT : usize = 3 ;
1949+
1950+ let mut members = Vec :: new ( ) ;
1951+
1952+ for i in 0 ..INSTANCE_COUNT {
1953+ let instance = create_stopped_instance_record (
1954+ & opctx,
1955+ & datastore,
1956+ & authz_project,
1957+ & format ! ( "instance-{i}" ) ,
1958+ )
1959+ . await ;
1960+
1961+ // Add the instance as a member to the group
1962+ let member = external:: AntiAffinityGroupMember :: Instance ( instance) ;
1963+ datastore
1964+ . anti_affinity_group_member_add (
1965+ & opctx,
1966+ & authz_aa_group,
1967+ member. clone ( ) ,
1968+ )
1969+ . await
1970+ . unwrap ( ) ;
1971+ members. push ( member) ;
1972+ }
1973+
1974+ for i in 0 ..AFFINITY_GROUP_COUNT {
1975+ let affinity_group = create_affinity_group (
1976+ & opctx,
1977+ & datastore,
1978+ & authz_project,
1979+ & format ! ( "affinity-{i}" ) ,
1980+ )
1981+ . await
1982+ . unwrap ( ) ;
1983+
1984+ // Add the instance as a member to the group
1985+ let member = external:: AntiAffinityGroupMember :: AffinityGroup (
1986+ AffinityGroupUuid :: from_untyped_uuid ( affinity_group. id ( ) ) ,
1987+ ) ;
1988+ datastore
1989+ . anti_affinity_group_member_add (
1990+ & opctx,
1991+ & authz_aa_group,
1992+ member. clone ( ) ,
1993+ )
1994+ . await
1995+ . unwrap ( ) ;
1996+ members. push ( member) ;
1997+ }
1998+
1999+ // Order by UUID, regardless of member type
2000+ members. sort_unstable_by ( |m1, m2| m1. id ( ) . cmp ( & m2. id ( ) ) ) ;
2001+
2002+ // We can list all members
2003+ let mut pagparams = DataPageParams {
2004+ marker : None ,
2005+ limit : NonZeroU32 :: new ( 100 ) . unwrap ( ) ,
2006+ direction : dropshot:: PaginationOrder :: Ascending ,
2007+ } ;
2008+ let observed_members = datastore
2009+ . anti_affinity_group_member_list (
2010+ & opctx,
2011+ & authz_aa_group,
2012+ & pagparams,
2013+ )
2014+ . await
2015+ . unwrap ( ) ;
2016+ assert_eq ! ( observed_members, members) ;
2017+
2018+ // We can paginate over the results
2019+ let marker = members[ 2 ] . id ( ) ;
2020+ pagparams. marker = Some ( & marker) ;
2021+ let observed_members = datastore
2022+ . anti_affinity_group_member_list (
2023+ & opctx,
2024+ & authz_aa_group,
2025+ & pagparams,
2026+ )
2027+ . await
2028+ . unwrap ( ) ;
2029+ assert_eq ! ( observed_members, members[ 3 ..] ) ;
2030+
2031+ // We can list limited results
2032+ pagparams. marker = Some ( & marker) ;
2033+ pagparams. limit = NonZeroU32 :: new ( 2 ) . unwrap ( ) ;
2034+ let observed_members = datastore
2035+ . anti_affinity_group_member_list (
2036+ & opctx,
2037+ & authz_aa_group,
2038+ & pagparams,
2039+ )
2040+ . await
2041+ . unwrap ( ) ;
2042+ assert_eq ! ( observed_members, members[ 3 ..5 ] ) ;
2043+
2044+ // We can list in descending order too
2045+ members. reverse ( ) ;
2046+ pagparams. marker = None ;
2047+ pagparams. limit = NonZeroU32 :: new ( 100 ) . unwrap ( ) ;
2048+ pagparams. direction = dropshot:: PaginationOrder :: Descending ;
2049+ let observed_members = datastore
2050+ . anti_affinity_group_member_list (
2051+ & opctx,
2052+ & authz_aa_group,
2053+ & pagparams,
2054+ )
2055+ . await
2056+ . unwrap ( ) ;
2057+ assert_eq ! ( observed_members, members) ;
2058+
2059+ // Clean up.
2060+ db. terminate ( ) . await ;
2061+ logctx. cleanup_successful ( ) ;
2062+ }
2063+
18932064 #[ tokio:: test]
18942065 async fn affinity_group_membership_add_remove_instance_with_vmm ( ) {
18952066 // Setup
0 commit comments