Skip to content

Commit

Permalink
CRDs for AllocationOverflow
Browse files Browse the repository at this point in the history
Implementation of yaml and backing Go code for the CRDs for supporting
Fleet Allocation Overflow tracking.

Work on googleforgames#2682
  • Loading branch information
markmandel committed Feb 16, 2023
1 parent ce56e29 commit eca02c4
Show file tree
Hide file tree
Showing 10 changed files with 586 additions and 0 deletions.
12 changes: 12 additions & 0 deletions install/helm/agones/templates/crds/fleet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ spec:
replicas:
type: integer
minimum: 0
allocationOverflow:
type: object
nullable: true
properties:
labels:
type: object
additionalProperties:
type: string
annotations:
type: object
additionalProperties:
type: string
scheduling:
type: string
enum:
Expand Down
12 changes: 12 additions & 0 deletions install/helm/agones/templates/crds/gameserverset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,18 @@ spec:
replicas:
type: integer
minimum: 0
allocationOverflow:
type: object
nullable: true
properties:
labels:
type: object
additionalProperties:
type: string
annotations:
type: object
additionalProperties:
type: string
scheduling:
type: string
enum:
Expand Down
24 changes: 24 additions & 0 deletions install/yaml/install.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,18 @@ spec:
replicas:
type: integer
minimum: 0
allocationOverflow:
type: object
nullable: true
properties:
labels:
type: object
additionalProperties:
type: string
annotations:
type: object
additionalProperties:
type: string
scheduling:
type: string
enum:
Expand Down Expand Up @@ -10331,6 +10343,18 @@ spec:
replicas:
type: integer
minimum: 0
allocationOverflow:
type: object
nullable: true
properties:
labels:
type: object
additionalProperties:
type: string
annotations:
type: object
additionalProperties:
type: string
scheduling:
type: string
enum:
Expand Down
89 changes: 89 additions & 0 deletions pkg/apis/agones/v1/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
apivalidation "k8s.io/apimachinery/pkg/api/validation"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
metav1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
Expand Down Expand Up @@ -107,3 +108,91 @@ func validateObjectMeta(objMeta *metav1.ObjectMeta) []metav1.StatusCause {
}
return causes
}

// AllocationOverflow specifies what labels and/or annotations to apply on Allocated GameServers
// if the desired number of the underlying `GameServerSet` drops below the number of Allocated GameServers
// attached to it.
type AllocationOverflow struct {
// Labels to be applied to the `GameServer`
// +optional
Labels map[string]string `json:"labels,omitempty"`
// Annotations to be applied to the `GameServer`
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
}

// Validate validates the label and annotation values
func (ao *AllocationOverflow) Validate() ([]metav1.StatusCause, bool) {
var causes []metav1.StatusCause
parentField := "Spec.AllocationOverflow"

errs := metav1validation.ValidateLabels(ao.Labels, field.NewPath(parentField))
if len(errs) != 0 {
for _, v := range errs {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Field: "labels",
Message: v.Error(),
})
}
}
errs = apivalidation.ValidateAnnotations(ao.Annotations,
field.NewPath(parentField))
if len(errs) != 0 {
for _, v := range errs {
causes = append(causes, metav1.StatusCause{
Type: metav1.CauseTypeFieldValueInvalid,
Field: "annotations",
Message: v.Error(),
})
}
}

return causes, len(causes) == 0
}

// CountMatches returns the number of Allocated GameServers that match the labels and annotations, and
// the set of GameServers left over.
func (ao *AllocationOverflow) CountMatches(list []*GameServer) (int32, []*GameServer) {
count := int32(0)
var rest []*GameServer
labelSelector := labels.Set(ao.Labels).AsSelector()
annotationSelector := labels.Set(ao.Annotations).AsSelector()

for _, gs := range list {
if gs.Status.State != GameServerStateAllocated {
continue
}
if !labelSelector.Matches(labels.Set(gs.ObjectMeta.Labels)) {
rest = append(rest, gs)
continue
}
if !annotationSelector.Matches(labels.Set(gs.ObjectMeta.Annotations)) {
rest = append(rest, gs)
continue
}
count++
}

return count, rest
}

// Apply applies the labels and annotations to the passed in GameServer
func (ao *AllocationOverflow) Apply(gs *GameServer) {
if ao.Annotations != nil {
if gs.ObjectMeta.Annotations == nil {
gs.ObjectMeta.Annotations = map[string]string{}
}
for k, v := range ao.Annotations {
gs.ObjectMeta.Annotations[k] = v
}
}
if ao.Labels != nil {
if gs.ObjectMeta.Labels == nil {
gs.ObjectMeta.Labels = map[string]string{}
}
for k, v := range ao.Labels {
gs.ObjectMeta.Labels[k] = v
}
}
}
208 changes: 208 additions & 0 deletions pkg/apis/agones/v1/common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// Copyright 2022 Google LLC All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package v1

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func TestAllocationOverflowValidate(t *testing.T) {
// valid
type expected struct {
valid bool
fields []string
}

fixtures := map[string]struct {
ao AllocationOverflow
expected
}{
"empty": {
ao: AllocationOverflow{},
expected: expected{
valid: true,
fields: nil,
},
},
"bad label name": {
ao: AllocationOverflow{
Labels: map[string]string{"$$$foobar": "stuff"},
Annotations: nil,
},
expected: expected{
valid: false,
fields: []string{"labels"},
},
},
"bad label value": {
ao: AllocationOverflow{
Labels: map[string]string{"valid": "$$$NOPE"},
Annotations: nil,
},
expected: expected{
valid: false,
fields: []string{"labels"},
},
},
"bad annotation name": {
ao: AllocationOverflow{
Labels: nil,
Annotations: map[string]string{"$$$foobar": "stuff"},
},
expected: expected{
valid: false,
fields: []string{"annotations"},
},
},
"valid full": {
ao: AllocationOverflow{
Labels: map[string]string{"valid": "yes", "still.valid": "check-me-out"},
Annotations: map[string]string{"icando-this": "yes, I can do all kinds of things here $$$"},
},
expected: expected{
valid: true,
fields: nil,
},
},
}

for k, v := range fixtures {
t.Run(k, func(t *testing.T) {
causes, valid := v.ao.Validate()
assert.Equal(t, v.expected.valid, valid, "valid")
if v.expected.fields == nil {
assert.Empty(t, causes)
} else {
for i, cause := range causes {
assert.Equal(t, metav1.CauseTypeFieldValueInvalid, cause.Type)
// messages come from K8s validation libraries, so testing exact matches would be brittle.
assert.Contains(t, cause.Message, "Invalid value:")
assert.Equal(t, v.expected.fields[i], cause.Field)
}
}
})
}
}

func TestAllocationOverflowCountMatches(t *testing.T) {
type expected struct {
count int32
rest int
}

fixtures := map[string]struct {
list func([]*GameServer)
ao func(*AllocationOverflow)
expected expected
}{
"simple": {
list: func(_ []*GameServer) {},
ao: func(_ *AllocationOverflow) {},
expected: expected{
count: 2,
rest: 0,
},
},
"label selector": {
list: func(list []*GameServer) {
list[0].ObjectMeta.Labels = map[string]string{"colour": "blue"}
},
ao: func(ao *AllocationOverflow) {
ao.Labels = map[string]string{"colour": "blue"}
},
expected: expected{
count: 1,
rest: 1,
},
},
"annotation selector": {
list: func(list []*GameServer) {
list[0].ObjectMeta.Annotations = map[string]string{"colour": "green"}
},
ao: func(ao *AllocationOverflow) {
ao.Annotations = map[string]string{"colour": "green"}
},
expected: expected{
count: 1,
rest: 1,
},
},
"both": {
list: func(list []*GameServer) {
list[0].ObjectMeta.Labels = map[string]string{"colour": "blue"}
list[0].ObjectMeta.Annotations = map[string]string{"colour": "green"}
},
ao: func(ao *AllocationOverflow) {
ao.Labels = map[string]string{"colour": "blue"}
ao.Annotations = map[string]string{"colour": "green"}
},
expected: expected{
count: 1,
rest: 1,
},
},
}

for k, v := range fixtures {
t.Run(k, func(t *testing.T) {
list := []*GameServer{
{ObjectMeta: metav1.ObjectMeta{Name: "g1"}, Status: GameServerStatus{State: GameServerStateAllocated}},
{ObjectMeta: metav1.ObjectMeta{Name: "g2"}, Status: GameServerStatus{State: GameServerStateAllocated}},
{ObjectMeta: metav1.ObjectMeta{Name: "g3"}, Status: GameServerStatus{State: GameServerStateReady}},
}
v.list(list)
ao := &AllocationOverflow{
Labels: nil,
Annotations: nil,
}
v.ao(ao)

count, rest := ao.CountMatches(list)
assert.Equal(t, v.expected.count, count, "count")
assert.Equal(t, v.expected.rest, len(rest), "rest")
for _, gs := range rest {
assert.Equal(t, GameServerStateAllocated, gs.Status.State)
}
})
}
}

func TestAllocationOverflowApply(t *testing.T) {
// check empty
gs := &GameServer{}
ao := AllocationOverflow{Labels: map[string]string{"colour": "green"}, Annotations: map[string]string{"colour": "blue", "map": "ice cream"}}

ao.Apply(gs)

require.Equal(t, ao.Annotations, gs.ObjectMeta.Annotations)
require.Equal(t, ao.Labels, gs.ObjectMeta.Labels)

// check append
ao = AllocationOverflow{Labels: map[string]string{"version": "1.0"}, Annotations: map[string]string{"version": "1.0"}}
ao.Apply(gs)

require.Equal(t, map[string]string{"colour": "green", "version": "1.0"}, gs.ObjectMeta.Labels)
require.Equal(t, map[string]string{"colour": "blue", "map": "ice cream", "version": "1.0"}, gs.ObjectMeta.Annotations)

// check overwrite
ao = AllocationOverflow{Labels: map[string]string{"colour": "red"}, Annotations: map[string]string{"colour": "green"}}
ao.Apply(gs)
require.Equal(t, map[string]string{"colour": "red", "version": "1.0"}, gs.ObjectMeta.Labels)
require.Equal(t, map[string]string{"colour": "green", "map": "ice cream", "version": "1.0"}, gs.ObjectMeta.Annotations)
}
Loading

0 comments on commit eca02c4

Please sign in to comment.