From f5bdb4d36c056998bee2f159cd53a08d98e57f95 Mon Sep 17 00:00:00 2001 From: igooch Date: Tue, 21 Nov 2023 11:44:08 -0800 Subject: [PATCH] Adds gameserverallocation e2e tests for Lists (#3516) --- pkg/apis/agones/v1/gameserver.go | 15 +- .../v1/gameserverallocation_test.go | 4 +- test/e2e/gameserverallocation_test.go | 247 +++++++++++++++++- 3 files changed, 250 insertions(+), 16 deletions(-) diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go index e2b1e4e0fb..797bd0723c 100644 --- a/pkg/apis/agones/v1/gameserver.go +++ b/pkg/apis/agones/v1/gameserver.go @@ -925,6 +925,7 @@ func (gs *GameServer) UpdateListCapacity(name string, capacity int64) error { } if list, ok := gs.Status.Lists[name]; ok { list.Capacity = capacity + list.Values = truncateList(list.Capacity, list.Values) gs.Status.Lists[name] = list return nil } @@ -940,16 +941,22 @@ func (gs *GameServer) AppendListValues(name string, values []string) error { mergedList := MergeRemoveDuplicates(list.Values, values) // Any duplicate values are silently dropped. list.Values = mergedList - // Truncate values if more than capacity - if len(list.Values) > int(list.Capacity) { - list.Values = append([]string{}, list.Values[:list.Capacity]...) - } + list.Values = truncateList(list.Capacity, list.Values) gs.Status.Lists[name] = list return nil } return errors.Errorf("unable to AppendListValues: Name %s, Values %s. List not found in GameServer %s", name, values, gs.ObjectMeta.GetName()) } +// truncateList truncates the list to the given capacity +func truncateList(capacity int64, list []string) []string { + if list == nil || len(list) <= int(capacity) { + return list + } + list = append([]string{}, list[:capacity]...) + return list +} + // MergeRemoveDuplicates merges two lists and removes any duplicate values. // Maintains ordering, so new values from list2 are appended to the end of list1. // Returns a new list with unique values only. diff --git a/pkg/apis/allocation/v1/gameserverallocation_test.go b/pkg/apis/allocation/v1/gameserverallocation_test.go index a6e67aac9f..1ecd2c2ec6 100644 --- a/pkg/apis/allocation/v1/gameserverallocation_test.go +++ b/pkg/apis/allocation/v1/gameserverallocation_test.go @@ -948,7 +948,7 @@ func TestGameServerListActions(t *testing.T) { want *agonesv1.GameServer wantErr bool }{ - "update list capacity": { + "update list capacity truncates list": { la: ListAction{ Capacity: int64Pointer(0), }, @@ -962,7 +962,7 @@ func TestGameServerListActions(t *testing.T) { want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{ Lists: map[string]agonesv1.ListStatus{ "pages": { - Values: []string{"page1", "page2"}, + Values: []string{}, Capacity: 0, }}}}, wantErr: false, diff --git a/test/e2e/gameserverallocation_test.go b/test/e2e/gameserverallocation_test.go index 0d514931e1..a6a104330b 100644 --- a/test/e2e/gameserverallocation_test.go +++ b/test/e2e/gameserverallocation_test.go @@ -261,7 +261,7 @@ func TestCreateFleetAndGameServerPlayerCapacityAllocation(t *testing.T) { require.NotEqual(t, gs1.ObjectMeta.Annotations["agones.dev/last-allocated"], gs2.ObjectMeta.Annotations["agones.dev/last-allocated"]) } -func TestCounterGameServerAllocation(t *testing.T) { +func TestCounterAndListGameServerAllocation(t *testing.T) { if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { t.SkipNow() } @@ -276,6 +276,12 @@ func TestCounterGameServerAllocation(t *testing.T) { Capacity: 10, }, } + flt.Spec.Template.Spec.Lists = map[string]agonesv1.ListStatus{ + "players": { + Values: []string{"player0"}, + Capacity: 10, + }, + } flt, err := client.Fleets(framework.Namespace).Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) require.NoError(t, err) @@ -295,7 +301,7 @@ func TestCounterGameServerAllocation(t *testing.T) { wantAllocated allocationv1.GameServerAllocationState // For a valid GSA: "allocated" if you expect the GSA to succed in allocating a GameServer, "unallocated" if not wantState agonesv1.GameServerState }{ - "Allocate to same GameServer MinAvailable (available capacity)": { + "Counter Allocate to same GameServer MinAvailable (available capacity)": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ Selectors: []allocationv1.GameServerSelector{ @@ -316,7 +322,28 @@ func TestCounterGameServerAllocation(t *testing.T) { wantAllocated: allocated, wantState: stateAllocated, }, - "Allocate to same GameServer MaxAvailable": { + "List Allocate to same GameServer MinAvailable (available capacity)": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Lists: map[string]allocationv1.ListSelector{ + "players": { + MinAvailable: 9, + }}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Lists: map[string]allocationv1.ListSelector{ + "players": { + MinAvailable: 9, + }}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, + "Counter Allocate to same GameServer MaxAvailable": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ Selectors: []allocationv1.GameServerSelector{ @@ -337,6 +364,27 @@ func TestCounterGameServerAllocation(t *testing.T) { wantAllocated: allocated, wantState: stateAllocated, }, + "List Allocate to same GameServer MaxAvailable": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Lists: map[string]allocationv1.ListSelector{ + "players": { + MaxAvailable: 9, + }}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Lists: map[string]allocationv1.ListSelector{ + "players": { + MaxAvailable: 9, + }}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, "Allocate to same GameServer MinCount (count value)": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ @@ -379,8 +427,29 @@ func TestCounterGameServerAllocation(t *testing.T) { wantAllocated: allocated, wantState: stateAllocated, }, + "List Allocate to same GameServer Contains Value": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Lists: map[string]allocationv1.ListSelector{ + "players": { + ContainsValue: "player0", + }}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Lists: map[string]allocationv1.ListSelector{ + "players": { + ContainsValue: "player0", + }}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, // 0 for MaxCount or MaxAvailable means unlimited maximum. Default for all fields: 0 - "Allocate to same GameServer no values": { + "Allocate to same GameServer no Counter values": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ Selectors: []allocationv1.GameServerSelector{ @@ -397,6 +466,23 @@ func TestCounterGameServerAllocation(t *testing.T) { wantAllocated: allocated, wantState: stateAllocated, }, + "Allocate to same GameServer no List values": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Lists: map[string]allocationv1.ListSelector{ + "players": {}}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Lists: map[string]allocationv1.ListSelector{ + "players": {}}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, "Counter does not exist": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ @@ -404,13 +490,26 @@ func TestCounterGameServerAllocation(t *testing.T) { LabelSelector: fleetSelector, GameServerState: &ready, Counters: map[string]allocationv1.CounterSelector{ - "lames": { + "players": { MinAvailable: 1, }}}}}}, wantGsaErr: false, wantAllocated: unallocated, }, - "MaxAvailable < MinAvailable": { + "List does not exist": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{{ + LabelSelector: fleetSelector, + GameServerState: &ready, + Lists: map[string]allocationv1.ListSelector{ + "games": { + MinAvailable: 1, + }}}}}}, + wantGsaErr: false, + wantAllocated: unallocated, + }, + "Counter MaxAvailable < MinAvailable": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ Selectors: []allocationv1.GameServerSelector{{ @@ -423,7 +522,20 @@ func TestCounterGameServerAllocation(t *testing.T) { }}}}}}, wantGsaErr: true, }, - "Maxcount < MinCount": { + "List MaxAvailable < MinAvailable": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{{ + LabelSelector: fleetSelector, + GameServerState: &ready, + Lists: map[string]allocationv1.ListSelector{ + "players": { + MaxAvailable: 8, + MinAvailable: 9, + }}}}}}, + wantGsaErr: true, + }, + "Counter Maxcount < MinCount": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ Selectors: []allocationv1.GameServerSelector{{ @@ -436,6 +548,19 @@ func TestCounterGameServerAllocation(t *testing.T) { }}}}}}, wantGsaErr: true, }, + "List Value does not exist": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{{ + LabelSelector: fleetSelector, + GameServerState: &ready, + Lists: map[string]allocationv1.ListSelector{ + "players": { + ContainsValue: "player1", + }}}}}}, + wantGsaErr: false, + wantAllocated: unallocated, + }, "Negative values for MinCount, MaxCount, MaxAvailable, MinAvailable": { gsa: allocationv1.GameServerAllocation{ Spec: allocationv1.GameServerAllocationSpec{ @@ -489,7 +614,7 @@ func TestCounterGameServerAllocation(t *testing.T) { require.NotEqual(t, gs1.ObjectMeta.ResourceVersion, gs2.ObjectMeta.ResourceVersion) require.NotEqual(t, gs1.ObjectMeta.Annotations["agones.dev/last-allocated"], gs2.ObjectMeta.Annotations["agones.dev/last-allocated"]) - // Reset any GameServers in state Allocated -> Ready. Note: This does not reset any changes to Counters. + // Reset any GameServers in state Allocated -> Ready. Note: This does not reset any changes to Counters or Lists. list, err := framework.ListGameServersFromFleet(flt) require.NoError(t, err) for _, gs := range list { @@ -703,9 +828,9 @@ func TestCounterGameServerAllocationActions(t *testing.T) { } // Reset any GameServers in state Allocated -> Ready, and reset any changes to Counters. - list, err := framework.ListGameServersFromFleet(flt) + gsList, err := framework.ListGameServersFromFleet(flt) require.NoError(t, err) - for _, gs := range list { + for _, gs := range gsList { if gs.Status.State == ready && cmp.Equal(gs.Status.Counters, counters) { continue } @@ -720,6 +845,108 @@ func TestCounterGameServerAllocationActions(t *testing.T) { } } +func TestListGameServerAllocationActions(t *testing.T) { + if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { + t.SkipNow() + } + t.Parallel() + ctx := context.Background() + client := framework.AgonesClient.AgonesV1() + + lists := map[string]agonesv1.ListStatus{} + lists["players"] = agonesv1.ListStatus{ + Values: []string{"player0", "player1", "player2"}, + Capacity: 8, + } + + flt := defaultFleet(framework.Namespace) + flt.Spec.Template.Spec.Lists = lists + + flt, err := client.Fleets(framework.Namespace).Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) + require.NoError(t, err) + defer client.Fleets(framework.Namespace).Delete(ctx, flt.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint:errcheck + framework.AssertFleetCondition(t, flt, e2e.FleetReadyCount(flt.Spec.Replicas)) + + fleetSelector := metav1.LabelSelector{MatchLabels: map[string]string{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}} + allocated := agonesv1.GameServerStateAllocated + ready := agonesv1.GameServerStateReady + one := int64(1) + + testCases := map[string]struct { + gsa allocationv1.GameServerAllocation + wantGsaErr bool + wantCapacity *int64 + wantValues []string + }{ + "add List values": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Lists: map[string]allocationv1.ListAction{ + "players": { + AddValues: []string{"player3", "player4", "player5"}, + }}}}, + wantGsaErr: false, + wantValues: []string{"player0", "player1", "player2", "player3", "player4", "player5"}, + }, + "change List capacity truncates": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Lists: map[string]allocationv1.ListAction{ + "players": { + Capacity: &one, + }}}}, + wantGsaErr: false, + wantValues: []string{"player0"}, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + + gsa, err := framework.AgonesClient.AllocationV1().GameServerAllocations(flt.ObjectMeta.Namespace).Create(ctx, testCase.gsa.DeepCopy(), metav1.CreateOptions{}) + if testCase.wantGsaErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, string(allocated), string(gsa.Status.State)) + + gs1, err := framework.AgonesClient.AgonesV1().GameServers(flt.ObjectMeta.Namespace).Get(ctx, gsa.Status.GameServerName, metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, allocated, gs1.Status.State) + assert.NotNil(t, gs1.ObjectMeta.Annotations["agones.dev/last-allocated"]) + + list, ok := gs1.Status.Lists["players"] + assert.True(t, ok) + if testCase.wantCapacity != nil { + assert.Equal(t, *testCase.wantCapacity, list.Capacity) + } + assert.Equal(t, testCase.wantValues, list.Values) + + // Reset any GameServers in state Allocated -> Ready, and reset any changes to Lists. + gsList, err := framework.ListGameServersFromFleet(flt) + require.NoError(t, err) + for _, gs := range gsList { + if gs.Status.State == ready && cmp.Equal(gs.Status.Lists, lists) { + continue + } + gsCopy := gs.DeepCopy() + gsCopy.Status.State = ready + gsCopy.Status.Lists = lists + reqReadyGs, err := framework.AgonesClient.AgonesV1().GameServers(framework.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{}) + require.NoError(t, err) + require.Equal(t, ready, reqReadyGs.Status.State) + } + }) + } +} + func TestMultiClusterAllocationOnLocalCluster(t *testing.T) { t.Parallel()