Skip to content

Commit

Permalink
CRDs for AllocationOverflow (#2979)
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 #2682
  • Loading branch information
markmandel committed Feb 24, 2023
1 parent c9a2e98 commit 2bdb487
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 @@ -10441,6 +10453,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 2023 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 2bdb487

Please sign in to comment.