diff --git a/examples/gameserverallocation.yaml b/examples/gameserverallocation.yaml index ce30aefa98..550508bbf1 100644 --- a/examples/gameserverallocation.yaml +++ b/examples/gameserverallocation.yaml @@ -51,6 +51,14 @@ spec: # label/annotation/player selectors to retrieve an already Allocated GameServer. gameServerState: Ready # [Stage:Alpha] + # [FeatureFlag:CountsAndLists] + # Counts and Lists provides the configuration for generic (player, room, session, etc.) tracking features. + # Commented out since Alpha, and disabled by default + # counters: + # players: + # minAvailable: 1 + # maxAvailable: 10 + # [Stage:Alpha] # [FeatureFlag:PlayerAllocationFilter] # Provides a filter on minimum and maximum values for player capacity when retrieving a GameServer # through Allocation. Defaults to no limits. diff --git a/pkg/apis/agones/v1/gameserver.go b/pkg/apis/agones/v1/gameserver.go index f171c8c731..d838c00d9e 100644 --- a/pkg/apis/agones/v1/gameserver.go +++ b/pkg/apis/agones/v1/gameserver.go @@ -879,16 +879,16 @@ func (gs *GameServer) UpdateCount(name string, action string, amount int64) erro cnt := counter.Count if action == GameServerPriorityIncrement { cnt += amount - // only check for Count > Capacity when incrementing - if cnt > counter.Capacity { - return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Incremented Count %d is greater than available capacity %d", name, action, amount, cnt, counter.Capacity) - } } else { cnt -= amount - // only check for Count < 0 when decrementing - if cnt < 0 { - return errors.Errorf("unable to UpdateCount with Name %s, Action %s, Amount %d. Decremented Count %d is less than 0", name, action, amount, cnt) - } + } + // Truncate to Capacity if Count > Capacity + if cnt > counter.Capacity { + cnt = counter.Capacity + } + // Truncate to Zero if Count is negative + if cnt < 0 { + cnt = 0 } counter.Count = cnt gs.Status.Counters[name] = counter @@ -904,6 +904,10 @@ func (gs *GameServer) UpdateCounterCapacity(name string, capacity int64) error { } if counter, ok := gs.Status.Counters[name]; ok { counter.Capacity = capacity + // If Capacity is now less than Count, reset Count here to equal Capacity + if counter.Count > counter.Capacity { + counter.Count = counter.Capacity + } gs.Status.Counters[name] = counter return nil } @@ -930,6 +934,7 @@ func (gs *GameServer) AppendListValues(name string, values []string) error { } if list, ok := gs.Status.Lists[name]; ok { mergedList := mergeRemoveDuplicates(list.Values, values) + // TODO: Truncate and apply up to cutoff if len(mergedList) > int(list.Capacity) { return errors.Errorf("unable to AppendListValues: Name %s, Values %s. Appended list length %d exceeds list capacity %d", name, values, len(mergedList), list.Capacity) } diff --git a/pkg/apis/agones/v1/gameserver_test.go b/pkg/apis/agones/v1/gameserver_test.go index 5855e0a632..f40a903153 100644 --- a/pkg/apis/agones/v1/gameserver_test.go +++ b/pkg/apis/agones/v1/gameserver_test.go @@ -1631,7 +1631,7 @@ func TestGameServerUpdateCount(t *testing.T) { amount: 1, wantErr: true, }, - "amount less than zero no-op and error": { + "negative amount no-op and error": { gs: GameServer{Status: GameServerStatus{ Counters: map[string]CounterStatus{ "foos": { @@ -1695,7 +1695,7 @@ func TestGameServerUpdateCount(t *testing.T) { }, wantErr: true, }, - "decrement beyond count no-op and error": { + "decrement beyond zero truncated": { gs: GameServer{Status: GameServerStatus{ Counters: map[string]CounterStatus{ "baz": { @@ -1706,12 +1706,12 @@ func TestGameServerUpdateCount(t *testing.T) { action: "Decrement", amount: 100, want: CounterStatus{ - Count: 99, + Count: 0, Capacity: 100, }, - wantErr: true, + wantErr: false, }, - "increment beyond capacity no-op and error": { + "increment beyond capacity truncated": { gs: GameServer{Status: GameServerStatus{ Counters: map[string]CounterStatus{ "splayers": { @@ -1722,10 +1722,10 @@ func TestGameServerUpdateCount(t *testing.T) { action: "Increment", amount: 2, want: CounterStatus{ - Count: 99, + Count: 100, Capacity: 100, }, - wantErr: true, + wantErr: false, }, } diff --git a/pkg/apis/allocation/v1/gameserverallocation_test.go b/pkg/apis/allocation/v1/gameserverallocation_test.go index 7fc3cc347f..2144502cef 100644 --- a/pkg/apis/allocation/v1/gameserverallocation_test.go +++ b/pkg/apis/allocation/v1/gameserverallocation_test.go @@ -836,7 +836,7 @@ func TestGameServerCounterActions(t *testing.T) { want *agonesv1.GameServer wantErr bool }{ - "update counter capacity": { + "update counter capacity and count is set to capacity": { ca: CounterAction{ Capacity: int64Pointer(0), }, @@ -850,12 +850,12 @@ func TestGameServerCounterActions(t *testing.T) { want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{ Counters: map[string]agonesv1.CounterStatus{ "mages": { - Count: 1, + Count: 0, Capacity: 0, }}}}, wantErr: false, }, - "fail update counter capacity and count": { + "fail update counter capacity and truncate update count": { ca: CounterAction{ Action: &INCREMENT, Amount: int64Pointer(10), @@ -871,7 +871,7 @@ func TestGameServerCounterActions(t *testing.T) { want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{ Counters: map[string]agonesv1.CounterStatus{ "sages": { - Count: 99, + Count: 100, Capacity: 100, }}}}, wantErr: true, @@ -912,7 +912,9 @@ func TestGameServerCounterActions(t *testing.T) { want: &agonesv1.GameServer{Status: agonesv1.GameServerStatus{ Counters: map[string]agonesv1.CounterStatus{ "heroes": { - Count: 1, + // Note: The Capacity is set first, and Count updated to not be greater than Capacity. + // Then the Count is decremented. See: gameserver.go/UpdateCounterCapacity + Count: 0, Capacity: 10, }}}}, wantErr: false, diff --git a/pkg/gameserverallocations/allocator_test.go b/pkg/gameserverallocations/allocator_test.go index 58cd88068b..c3e630edfc 100644 --- a/pkg/gameserverallocations/allocator_test.go +++ b/pkg/gameserverallocations/allocator_test.go @@ -370,7 +370,7 @@ func TestAllocatorApplyAllocationToGameServerCountsListsActions(t *testing.T) { Capacity: 40, }}, }, - "CounterActions and ListActions only update list capacity": { + "CounterActions and ListActions truncate counter Count and update list capacity": { features: fmt.Sprintf("%s=true", runtime.FeatureCountsAndLists), gs: &gs2, gsa: &allocationv1.GameServerAllocation{ @@ -392,7 +392,7 @@ func TestAllocatorApplyAllocationToGameServerCountsListsActions(t *testing.T) { }}}}, wantCounters: map[string]agonesv1.CounterStatus{ "rooms": { - Count: 101, + Count: 1000, Capacity: 1000, }}, wantLists: map[string]agonesv1.ListStatus{ diff --git a/test/e2e/fleet_test.go b/test/e2e/fleet_test.go index 318486ec74..3c11e4e95a 100644 --- a/test/e2e/fleet_test.go +++ b/test/e2e/fleet_test.go @@ -1621,7 +1621,8 @@ func TestFleetAggregatedCounterStatus(t *testing.T) { } flt, err := client.Fleets(framework.Namespace).Create(ctx, flt.DeepCopy(), metav1.CreateOptions{}) - assert.NoError(t, err) + 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)) // allocate two of them. @@ -1650,21 +1651,19 @@ func TestFleetAggregatedCounterStatus(t *testing.T) { allocatedCapacity := 0 allocatedCount := 0 // set random counts and capacities for each gameserver - for i := range list { - // Do this, otherwise scopelint complains about "using a reference for the variable on range scope" - gs := &list[i] + for _, gs := range list { count := rand.IntnRange(2, 9) capacity := rand.IntnRange(count, 100) totalCapacity += capacity msg := fmt.Sprintf("SET_COUNTER_CAPACITY games %d", capacity) - reply, err := framework.SendGameServerUDP(t, gs, msg) + reply, err := framework.SendGameServerUDP(t, &gs, msg) require.NoError(t, err) assert.Equal(t, "true", reply) totalCount += count msg = fmt.Sprintf("SET_COUNTER_COUNT games %d", count) - reply, err = framework.SendGameServerUDP(t, gs, msg) + reply, err = framework.SendGameServerUDP(t, &gs, msg) require.NoError(t, err) assert.Equal(t, "true", reply) diff --git a/test/e2e/gameserver_test.go b/test/e2e/gameserver_test.go index e9c164de31..74e7319c8e 100644 --- a/test/e2e/gameserver_test.go +++ b/test/e2e/gameserver_test.go @@ -1226,7 +1226,7 @@ func TestCountsAndLists(t *testing.T) { t.SkipNow() } t.Parallel() - + ctx := context.Background() gs := framework.DefaultGameServer(framework.Namespace) gs.Spec.Counters = make(map[string]agonesv1.CounterStatus) @@ -1253,7 +1253,7 @@ func TestCountsAndLists(t *testing.T) { gs, err := framework.CreateGameServerAndWaitUntilReady(t, framework.Namespace, gs) require.NoError(t, err) - + defer framework.AgonesClient.AgonesV1().GameServers(framework.Namespace).Delete(ctx, gs.ObjectMeta.Name, metav1.DeleteOptions{}) // nolint: errcheck assert.Equal(t, agonesv1.GameServerStateReady, gs.Status.State) testCases := map[string]struct { diff --git a/test/e2e/gameserverallocation_test.go b/test/e2e/gameserverallocation_test.go index 8dfb0a813d..616cedc965 100644 --- a/test/e2e/gameserverallocation_test.go +++ b/test/e2e/gameserverallocation_test.go @@ -17,6 +17,7 @@ package e2e import ( "context" "fmt" + "sort" "sync" "testing" "time" @@ -27,11 +28,13 @@ import ( multiclusterv1 "agones.dev/agones/pkg/apis/multicluster/v1" "agones.dev/agones/pkg/util/runtime" e2e "agones.dev/agones/test/e2e/framework" + "github.com/google/go-cmp/cmp" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/uuid" "k8s.io/apimachinery/pkg/util/wait" ) @@ -260,6 +263,579 @@ 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) { + if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { + t.SkipNow() + } + t.Parallel() + ctx := context.Background() + client := framework.AgonesClient.AgonesV1() + + flt := defaultFleet(framework.Namespace) + flt.Spec.Template.Spec.Counters = map[string]agonesv1.CounterStatus{ + "games": { + Count: 2, + Capacity: 10, + }, + } + + 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)) + + // Need fleetSelector to get the correct fleet, otherwise GSA will return game servers from any fleet in the namespace. + fleetSelector := metav1.LabelSelector{MatchLabels: map[string]string{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}} + stateAllocated := agonesv1.GameServerStateAllocated + ready := agonesv1.GameServerStateReady + allocated := allocationv1.GameServerAllocationAllocated + unallocated := allocationv1.GameServerAllocationUnAllocated + + testCases := map[string]struct { + gsa allocationv1.GameServerAllocation + wantGsaErr bool // For invalid GSA + 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)": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MinAvailable: 5, + }}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MinAvailable: 5, + }}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, + "Allocate to same GameServer MaxAvailable": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MaxAvailable: 10, + }}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MaxAvailable: 10, + }}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, + "Allocate to same GameServer MinCount (count value)": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MinCount: 2, + }}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MinCount: 1, + }}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, + "Allocate to same GameServer MaxCount (count value)": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MaxCount: 3, + }}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MaxCount: 2, + }}}}}}, + 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": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + { + LabelSelector: fleetSelector, + GameServerState: &stateAllocated, + Counters: map[string]allocationv1.CounterSelector{ + "games": {}}}, { + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": {}}}}}}, + wantGsaErr: false, + wantAllocated: allocated, + wantState: stateAllocated, + }, + "Counter does not exist": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{{ + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "lames": { + MinAvailable: 1, + }}}}}}, + wantGsaErr: false, + wantAllocated: unallocated, + }, + "MaxAvailable < MinAvailable": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{{ + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MaxAvailable: 1, + MinAvailable: 2, + }}}}}}, + wantGsaErr: true, + }, + "Maxcount < MinCount": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{{ + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MaxCount: 1, + MinCount: 2, + }}}}}}, + wantGsaErr: true, + }, + "Negative values for MinCount, MaxCount, MaxAvailable, MinAvailable": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{{ + LabelSelector: fleetSelector, + GameServerState: &ready, + Counters: map[string]allocationv1.CounterSelector{ + "games": { + MaxCount: -1, + MinCount: -2, + MaxAvailable: -10, + MinAvailable: -1, + }}}}}}, + wantGsaErr: true, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + + // First allocation + 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(testCase.wantAllocated), string(gsa.Status.State)) + + gs1, err := framework.AgonesClient.AgonesV1().GameServers(flt.ObjectMeta.Namespace).Get(ctx, gsa.Status.GameServerName, metav1.GetOptions{}) + if testCase.wantAllocated == unallocated { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, testCase.wantState, gs1.Status.State) + assert.NotNil(t, gs1.ObjectMeta.Annotations["agones.dev/last-allocated"]) + + // Second allocation + gsa, err = framework.AgonesClient.AllocationV1().GameServerAllocations(flt.ObjectMeta.Namespace).Create(ctx, gsa.DeepCopy(), metav1.CreateOptions{}) + require.NoError(t, err) + assert.Equal(t, string(testCase.wantAllocated), string(gsa.Status.State)) + assert.Equal(t, gs1.ObjectMeta.Name, gsa.Status.GameServerName) + + gs2, err := framework.AgonesClient.AgonesV1().GameServers(flt.ObjectMeta.Namespace).Get(ctx, gsa.Status.GameServerName, metav1.GetOptions{}) + require.NoError(t, err) + assert.Equal(t, testCase.wantState, gs2.Status.State) + + // Confirm allocated to the same GameServer (gs2 == gs1) + require.Equal(t, gs1.ObjectMeta.Name, gs2.ObjectMeta.Name) + 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. + list, err := framework.ListGameServersFromFleet(flt) + require.NoError(t, err) + for _, gs := range list { + if gs.Status.State == ready { + continue + } + gsCopy := gs.DeepCopy() + gsCopy.Status.State = ready + 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 TestCounterGameServerAllocationSorting(t *testing.T) { + if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { + t.SkipNow() + } + t.Parallel() + ctx := context.Background() + client := framework.AgonesClient.AgonesV1() + + flt := defaultFleet(framework.Namespace) + flt.Spec.Template.Spec.Counters = map[string]agonesv1.CounterStatus{ + "games": { + Count: 0, + Capacity: 10, + }, + } + + 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)) + + list, err := framework.ListGameServersFromFleet(flt) + assert.NoError(t, err) + // Key: GameServer name, Value: available capacity + gameServers := map[string]int{} + // Set random counts and capacities for each gameserver + for _, gs := range list { + count := rand.IntnRange(0, 99) // Available Capacity will be at least 1 + capacity := rand.IntnRange(count, 100) + availableCapacity := capacity - count + gameServers[gs.ObjectMeta.Name] = availableCapacity + + gsCopy := gs.DeepCopy() + gsCopy.Status.Counters["games"] = agonesv1.CounterStatus{ + Count: int64(count), + Capacity: int64(capacity), + } + _, err := framework.AgonesClient.AgonesV1().GameServers(framework.Namespace).Update(ctx, gsCopy, metav1.UpdateOptions{}) + require.NoError(t, err) + } + // GameServers names sorted by available capacity, ascending. + sortedGs := make([]string, 0, len(gameServers)) + for gsName := range gameServers { + sortedGs = append(sortedGs, gsName) + } + sort.Slice(sortedGs, func(i, j int) bool { return gameServers[sortedGs[i]] < gameServers[sortedGs[j]] }) + + fleetSelector := metav1.LabelSelector{MatchLabels: map[string]string{agonesv1.FleetNameLabel: flt.ObjectMeta.Name}} + ready := agonesv1.GameServerStateReady + allocated := agonesv1.GameServerStateAllocated + + testCases := map[string]struct { + gsa allocationv1.GameServerAllocation + wantGameServer string // GameServer Name + }{ + "Allocation sorting by count value, ascending": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Priorities: []agonesv1.Priority{ + {Type: agonesv1.GameServerPriorityCounter, + Key: "games", + Order: agonesv1.GameServerPriorityAscending}, + }}}, + wantGameServer: sortedGs[0], + }, + "Allocation sorting by count value, descending": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Priorities: []agonesv1.Priority{ + {Type: agonesv1.GameServerPriorityCounter, + Key: "games", + Order: agonesv1.GameServerPriorityDescending}, + }}}, + wantGameServer: sortedGs[2], + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + + // Allocate GameServer + gsa, err := framework.AgonesClient.AllocationV1().GameServerAllocations(flt.ObjectMeta.Namespace).Create(ctx, testCase.gsa.DeepCopy(), metav1.CreateOptions{}) + 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"]) + assert.Equal(t, testCase.wantGameServer, gs1.ObjectMeta.Name) + + // Reset any GameServers in state Allocated -> Ready. Note: This does not reset any changes to Counters. + list, err := framework.ListGameServersFromFleet(flt) + require.NoError(t, err) + for _, gs := range list { + if gs.Status.State == ready { + continue + } + gsCopy := gs.DeepCopy() + gsCopy.Status.State = ready + 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 TestCounterGameServerAllocationActions(t *testing.T) { + if !runtime.FeatureEnabled(runtime.FeatureCountsAndLists) { + t.SkipNow() + } + t.Parallel() + ctx := context.Background() + client := framework.AgonesClient.AgonesV1() + + counters := map[string]agonesv1.CounterStatus{} + counters["games"] = agonesv1.CounterStatus{ + Count: 5, + Capacity: 10, + } + + flt := defaultFleet(framework.Namespace) + flt.Spec.Template.Spec.Counters = counters + + 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 + increment := agonesv1.GameServerPriorityIncrement + decrement := agonesv1.GameServerPriorityDecrement + zero := int64(0) + one := int64(1) + five := int64(5) + six := int64(6) + ten := int64(10) + negativeOne := int64(-1) + + testCases := map[string]struct { + gsa allocationv1.GameServerAllocation + wantGsaErr bool + wantCount *int64 + wantCapacity *int64 + }{ + "increment": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Action: &increment, + Amount: &one, + }}}}, + wantGsaErr: false, + wantCount: &six, + wantCapacity: &ten, + }, + "decrement": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Action: &decrement, + Amount: &five, + }}}}, + wantGsaErr: false, + wantCount: &zero, + wantCapacity: &ten, + }, + "change capacity to less than count also updates count": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Capacity: &zero, + }}}}, + wantGsaErr: false, + wantCount: &zero, + wantCapacity: &zero, + }, + "decrement past zero truncated": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Action: &decrement, + Amount: &six, + }}}}, + wantGsaErr: false, + wantCount: &zero, + wantCapacity: &ten, + }, + "decrement negative": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Action: &decrement, + Amount: &negativeOne, + }}}}, + wantGsaErr: true, + }, + "increment past capacity truncated": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Action: &increment, + Amount: &six, + }}}}, + wantGsaErr: false, + wantCount: &ten, + wantCapacity: &ten, + }, + "increment negative": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Action: &increment, + Amount: &negativeOne, + }}}}, + wantGsaErr: true, + }, + "change capacity negative": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "games": { + Capacity: &negativeOne, + }}}}, + wantGsaErr: true, + }, + // Note: a gameserver is still allocated even though the counter does not exist (and thus the + // action cannot be performed). gsa.Validate() is not able to see the state of Counters in the + // fleet, so the GSA is not able to validate the existence of a Counter. Use the + // GameServerSelector to filter the Counters. + "Counter does not exist": { + gsa: allocationv1.GameServerAllocation{ + Spec: allocationv1.GameServerAllocationSpec{ + Selectors: []allocationv1.GameServerSelector{ + {LabelSelector: fleetSelector}, + }, + Counters: map[string]allocationv1.CounterAction{ + "lames": { + Action: &increment, + Amount: &one, + }}}}, + wantGsaErr: false, + }, + } + + 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"]) + + counter, ok := gs1.Status.Counters["games"] + assert.True(t, ok) + if testCase.wantCount != nil { + assert.Equal(t, *testCase.wantCount, counter.Count) + } + if testCase.wantCapacity != nil { + assert.Equal(t, *testCase.wantCapacity, counter.Capacity) + } + + // Reset any GameServers in state Allocated -> Ready, and reset any changes to Counters. + list, err := framework.ListGameServersFromFleet(flt) + require.NoError(t, err) + for _, gs := range list { + if gs.Status.State == ready && cmp.Equal(gs.Status.Counters, counters) { + continue + } + gsCopy := gs.DeepCopy() + gsCopy.Status.State = ready + gsCopy.Status.Counters = counters + 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()