diff --git a/build/install.yaml b/build/install.yaml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cmd/controller/main.go b/cmd/controller/main.go index c388cc3c8f..adfa6c0cc4 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -26,6 +26,7 @@ import ( "agones.dev/agones/pkg/client/clientset/versioned" "agones.dev/agones/pkg/client/informers/externalversions" "agones.dev/agones/pkg/gameservers" + "agones.dev/agones/pkg/gameserversets" "agones.dev/agones/pkg/util/runtime" "agones.dev/agones/pkg/util/signals" "agones.dev/agones/pkg/util/webhooks" @@ -128,7 +129,8 @@ func main() { agonesInformerFactory := externalversions.NewSharedInformerFactory(agonesClient, 30*time.Second) kubeInformationFactory := informers.NewSharedInformerFactory(kubeClient, 30*time.Second) - c := gameservers.NewController(wh, health, minPort, maxPort, sidecarImage, alwaysPullSidecar, kubeClient, kubeInformationFactory, extClient, agonesClient, agonesInformerFactory) + gsController := gameservers.NewController(wh, health, minPort, maxPort, sidecarImage, alwaysPullSidecar, kubeClient, kubeInformationFactory, extClient, agonesClient, agonesInformerFactory) + gsSetController := gameserversets.NewController(wh, health, kubeClient, extClient, agonesClient, agonesInformerFactory) stop := signals.NewStopChannel() @@ -140,6 +142,18 @@ func main() { logger.WithError(err).Fatal("could not run webhook server") } }() + go func() { + err = gsController.Run(2, stop) + if err != nil { + logger.WithError(err).Fatal("Could not run gameserver controller") + } + }() + go func() { + err = gsSetController.Run(2, stop) + if err != nil { + logger.WithError(err).Fatal("Could not run gameserverset controller") + } + }() go func() { logger.Info("Starting health check...") srv := &http.Server{ @@ -158,10 +172,6 @@ func main() { } }() - err = c.Run(2, stop) - if err != nil { - logger.WithError(err).Fatal("Could not run gameserver controller") - } - - logger.Info("Shut down gameserver controller") + <-stop + logger.Info("Shut down agones controllers") } diff --git a/examples/cpp-simple/gameserverset.yaml b/examples/cpp-simple/gameserverset.yaml new file mode 100644 index 0000000000..a7514d7120 --- /dev/null +++ b/examples/cpp-simple/gameserverset.yaml @@ -0,0 +1,33 @@ +# Copyright 2018 Google Inc. 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. + +# Usually you would define a Fleet rather than a GameServerSet +# directly. This is here mostly for testing purposes + +apiVersion: "stable.agones.dev/v1alpha1" +kind: GameServerSet +metadata: + name: cpp-simple +spec: + replicas: 5 + template: + spec: + containerPort: 7654 + template: + spec: + health: + initialDelaySeconds: 15 + containers: + - name: cpp-simple + image: gcr.io/agones-images/cpp-simple-server:0.1 \ No newline at end of file diff --git a/examples/simple-udp/server/gameserverset.yaml b/examples/simple-udp/server/gameserverset.yaml new file mode 100644 index 0000000000..a02fea1f62 --- /dev/null +++ b/examples/simple-udp/server/gameserverset.yaml @@ -0,0 +1,31 @@ +# Copyright 2018 Google Inc. 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. + +# Usually you would define a Fleet rather than a GameServerSet +# directly. This is here mostly for testing purposes + +apiVersion: "stable.agones.dev/v1alpha1" +kind: GameServerSet +metadata: + name: simple-udp +spec: + replicas: 2 + template: + spec: + containerPort: 7654 + template: + spec: + containers: + - name: simple-udp + image: gcr.io/agones-images/udp-server:0.1 \ No newline at end of file diff --git a/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml new file mode 100644 index 0000000000..7f574430a1 --- /dev/null +++ b/install/helm/agones/templates/crds/_gameserverspecvalidation.yaml @@ -0,0 +1,108 @@ +# Copyright 2018 Google Inc. 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. + +{{/* Validation for a gameserver spec */}} +{{- define "gameserver.validation" }} +required: +- spec +properties: + spec: + required: + - containerPort + - template + properties: + template: + type: object + required: + - spec + properties: + spec: + type: object + required: + - containers + properties: + containers: + type: array + items: + type: object + required: + - image + properties: + name: + type: string + minLength: 0 + maxLength: 63 + pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + image: + type: string + minLength: 1 + minItems: 1 + container: + title: The container name running the gameserver + description: if there is more than one container, specify which one is the game server + type: string + minLength: 0 + maxLength: 63 + pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + portPolicy: + title: the port policy that will be applied to the game server + description: | + portPolicy has two options: + - "dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to + - "static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the + port is available. When static is the policy specified, `hostPort` is required to be populated + type: string + enum: + - dynamic + - static + protocol: + title: Protocol being used. Defaults to UDP. TCP is the only other option + type: string + enum: + - UDP + - TCP + containerPort: + title: The port that is being opened on the game server process + type: integer + minimum: 0 + maximum: 65535 + hostPort: + title: The port exposed on the host + description: Only required when `portPolicy` is "static". Overwritten when portPolicy is "dynamic". + type: integer + minimum: 0 + maximum: 65535 + health: + type: object + title: Health checking for the running game server + properties: + disabled: + title: Disable health checking. defaults to false, but can be set to true + type: boolean + initialDelaySeconds: + title: Number of seconds after the container has started before health check is initiated. Defaults to 5 seconds + type: integer + minimum: 0 + maximum: 2147483648 + periodSeconds: + title: How long before the server is considered not healthy + type: integer + minimum: 0 + maximum: 2147483648 + failureThreshold: + title: Minimum consecutive failures for the health probe to be considered failed after having succeeded. + type: integer + minimum: 1 + maximum: 2147483648 +{{- end }} \ No newline at end of file diff --git a/install/helm/agones/templates/crds/gameserver.yaml b/install/helm/agones/templates/crds/gameserver.yaml index 4806fbc76f..63513259e4 100644 --- a/install/helm/agones/templates/crds/gameserver.yaml +++ b/install/helm/agones/templates/crds/gameserver.yaml @@ -34,94 +34,4 @@ spec: singular: gameserver validation: openAPIV3Schema: - required: - - spec - properties: - spec: - required: - - containerPort - - template - properties: - template: - type: object - required: - - spec - properties: - spec: - type: object - required: - - containers - properties: - containers: - type: array - items: - type: object - required: - - image - properties: - name: - type: string - minLength: 0 - maxLength: 63 - pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" - image: - type: string - minLength: 1 - minItems: 1 - container: - title: The container name running the gameserver - description: if there is more than one container, specify which one is the game server - type: string - minLength: 0 - maxLength: 63 - pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" - portPolicy: - title: the port policy that will be applied to the game server - description: | - portPolicy has two options: - - "dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to - - "static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the - port is available. When static is the policy specified, `hostPort` is required to be populated - type: string - enum: - - dynamic - - static - protocol: - title: Protocol being used. Defaults to UDP. TCP is the only other option - type: string - enum: - - UDP - - TCP - containerPort: - title: The port that is being opened on the game server process - type: integer - minimum: 0 - maximum: 65535 - hostPort: - title: The port exposed on the host - description: Only required when `portPolicy` is "static". Overwritten when portPolicy is "dynamic". - type: integer - minimum: 0 - maximum: 65535 - health: - type: object - title: Health checking for the running game server - properties: - disabled: - title: Disable health checking. defaults to false, but can be set to true - type: boolean - initialDelaySeconds: - title: Number of seconds after the container has started before health check is initiated. Defaults to 5 seconds - type: integer - minimum: 0 - maximum: 2147483648 - periodSeconds: - title: How long before the server is considered not healthy - type: integer - minimum: 0 - maximum: 2147483648 - failureThreshold: - title: Minimum consecutive failures for the health probe to be considered failed after having succeeded. - type: integer - minimum: 1 - maximum: 2147483648 \ No newline at end of file + {{- include "gameserver.validation" . | indent 6 }} \ No newline at end of file diff --git a/install/helm/agones/templates/crds/gameserverset.yaml b/install/helm/agones/templates/crds/gameserverset.yaml new file mode 100644 index 0000000000..370b3a79d3 --- /dev/null +++ b/install/helm/agones/templates/crds/gameserverset.yaml @@ -0,0 +1,48 @@ +# Copyright 2018 Google Inc. 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. + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: gameserversets.stable.agones.dev + labels: + component: crd + app: {{ template "agones.name" . }} + chart: {{ template "agones.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + group: stable.agones.dev + version: v1alpha1 + scope: Namespaced + names: + kind: GameServerSet + plural: gameserversets + shortNames: + - gss + - gsset + singular: gameserverset + validation: + openAPIV3Schema: + properties: + spec: + required: + - replicas + - template + properties: + replicas: + type: integer + minimum: 0 + template: + {{- include "gameserver.validation" . | indent 14 }} \ No newline at end of file diff --git a/install/helm/agones/templates/serviceaccounts/controller.yaml b/install/helm/agones/templates/serviceaccounts/controller.yaml index 3961e82836..3d4c6ae872 100644 --- a/install/helm/agones/templates/serviceaccounts/controller.yaml +++ b/install/helm/agones/templates/serviceaccounts/controller.yaml @@ -47,8 +47,8 @@ rules: resources: ["customresourcedefinitions"] verbs: ["get"] - apiGroups: ["stable.agones.dev"] - resources: ["gameservers"] - verbs: ["delete", "get", "list", "update", "watch"] + resources: ["gameservers", "gameserversets"] + verbs: ["create", "delete", "get", "list", "update", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/install/helm/agones/templates/validatingwebhook.yaml b/install/helm/agones/templates/validatingwebhook.yaml index 2153e76fa4..4655937060 100644 --- a/install/helm/agones/templates/validatingwebhook.yaml +++ b/install/helm/agones/templates/validatingwebhook.yaml @@ -35,4 +35,12 @@ webhooks: apiVersions: - "v1alpha1" operations: - - CREATE \ No newline at end of file + - CREATE + - apiGroups: + - stable.agones.dev + resources: + - "gameserversets" + apiVersions: + - "v1alpha1" + operations: + - UPDATE \ No newline at end of file diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index 478526ce15..58639cd4f4 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -74,8 +74,8 @@ rules: resources: ["customresourcedefinitions"] verbs: ["get"] - apiGroups: ["stable.agones.dev"] - resources: ["gameservers"] - verbs: ["delete", "get", "list", "update", "watch"] + resources: ["gameservers", "gameserversets"] + verbs: ["create", "delete", "get", "list", "update", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding @@ -192,7 +192,7 @@ spec: - gs singular: gameserver validation: - openAPIV3Schema: + openAPIV3Schema: required: - spec properties: @@ -285,6 +285,146 @@ spec: minimum: 1 maximum: 2147483648 --- +# Source: agones/templates/crds/gameserverset.yaml +# Copyright 2018 Google Inc. 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. + +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: gameserversets.stable.agones.dev + labels: + component: crd + app: agones + chart: agones-0.2.0 + release: agones-manual + heritage: Tiller +spec: + group: stable.agones.dev + version: v1alpha1 + scope: Namespaced + names: + kind: GameServerSet + plural: gameserversets + shortNames: + - gss + - gsset + singular: gameserverset + validation: + openAPIV3Schema: + properties: + spec: + required: + - replicas + - template + properties: + replicas: + type: integer + minimum: 0 + template: + required: + - spec + properties: + spec: + required: + - containerPort + - template + properties: + template: + type: object + required: + - spec + properties: + spec: + type: object + required: + - containers + properties: + containers: + type: array + items: + type: object + required: + - image + properties: + name: + type: string + minLength: 0 + maxLength: 63 + pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + image: + type: string + minLength: 1 + minItems: 1 + container: + title: The container name running the gameserver + description: if there is more than one container, specify which one is the game server + type: string + minLength: 0 + maxLength: 63 + pattern: "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$" + portPolicy: + title: the port policy that will be applied to the game server + description: | + portPolicy has two options: + - "dynamic" (default) the system allocates a free hostPort for the gameserver, for game clients to connect to + - "static", user defines the hostPort that the game client will connect to. Then onus is on the user to ensure that the + port is available. When static is the policy specified, `hostPort` is required to be populated + type: string + enum: + - dynamic + - static + protocol: + title: Protocol being used. Defaults to UDP. TCP is the only other option + type: string + enum: + - UDP + - TCP + containerPort: + title: The port that is being opened on the game server process + type: integer + minimum: 0 + maximum: 65535 + hostPort: + title: The port exposed on the host + description: Only required when `portPolicy` is "static". Overwritten when portPolicy is "dynamic". + type: integer + minimum: 0 + maximum: 65535 + health: + type: object + title: Health checking for the running game server + properties: + disabled: + title: Disable health checking. defaults to false, but can be set to true + type: boolean + initialDelaySeconds: + title: Number of seconds after the container has started before health check is initiated. Defaults to 5 seconds + type: integer + minimum: 0 + maximum: 2147483648 + periodSeconds: + title: How long before the server is considered not healthy + type: integer + minimum: 0 + maximum: 2147483648 + failureThreshold: + title: Minimum consecutive failures for the health probe to be considered failed after having succeeded. + type: integer + minimum: 1 + maximum: 2147483648 +--- # Source: agones/templates/service.yaml # Copyright 2018 Google Inc. All Rights Reserved. # @@ -466,3 +606,11 @@ webhooks: - "v1alpha1" operations: - CREATE + - apiGroups: + - stable.agones.dev + resources: + - "gameserversets" + apiVersions: + - "v1alpha1" + operations: + - UPDATE diff --git a/pkg/apis/stable/v1alpha1/types.go b/pkg/apis/stable/v1alpha1/gameserver.go similarity index 97% rename from pkg/apis/stable/v1alpha1/types.go rename to pkg/apis/stable/v1alpha1/gameserver.go index a4ce3ba515..87fd6b0e63 100644 --- a/pkg/apis/stable/v1alpha1/types.go +++ b/pkg/apis/stable/v1alpha1/gameserver.go @@ -44,6 +44,8 @@ const ( Error State = "Error" // Unhealthy is when the GameServer has failed its health checks Unhealthy State = "Unhealthy" + // Allocated is when the GameServer has been allocated to a session + Allocated State = "Allocated" // Static PortPolicy means that the user defines the hostPort to be used // in the configuration. @@ -85,6 +87,22 @@ type GameServer struct { Status GameServerStatus `json:"status"` } +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// GameServerList is a list of GameServer resources +type GameServerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GameServer `json:"items"` +} + +// GameServerTemplateSpec is a template for GameServers +type GameServerTemplateSpec struct { + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec GameServerSpec `json:"spec"` +} + // GameServerSpec is the spec for a GameServer resource type GameServerSpec struct { // Container specifies which Pod container is the game server. Only required if there is more than one @@ -135,16 +153,6 @@ type GameServerStatus struct { NodeName string `json:"nodeName"` } -// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object - -// GameServerList is a list of GameServer resources -type GameServerList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - - Items []GameServer `json:"items"` -} - // ApplyDefaults applies default values to the GameServer if they are not already populated func (gs *GameServer) ApplyDefaults() { gs.ObjectMeta.Finalizers = append(gs.ObjectMeta.Finalizers, stable.GroupName) diff --git a/pkg/apis/stable/v1alpha1/types_test.go b/pkg/apis/stable/v1alpha1/gameserver_test.go similarity index 100% rename from pkg/apis/stable/v1alpha1/types_test.go rename to pkg/apis/stable/v1alpha1/gameserver_test.go diff --git a/pkg/apis/stable/v1alpha1/gameserverset.go b/pkg/apis/stable/v1alpha1/gameserverset.go new file mode 100644 index 0000000000..0a26f82fab --- /dev/null +++ b/pkg/apis/stable/v1alpha1/gameserverset.go @@ -0,0 +1,112 @@ +// Copyright 2018 Google Inc. 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 v1alpha1 + +import ( + "reflect" + + "agones.dev/agones/pkg/apis/stable" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // GameServerSetGameServerLabel is the label that the name of the GameServerSet + // is set on the GameServer the GameServerSet controls + GameServerSetGameServerLabel = stable.GroupName + "/gameserverset" +) + +// +genclient +// +genclient:noStatus +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// GameServerSet is the data structure a set of GameServers +// This matches philosophically with the relationship between +// Depoyments and ReplicaSets +type GameServerSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GameServerSetSpec `json:"spec"` + Status GameServerSetStatus `json:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// GameServerSetList is a list of GameServerSet resources +type GameServerSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []GameServerSet `json:"items"` +} + +// GameServerSetSpec the specification for +type GameServerSetSpec struct { + // Replicas are the number of GameServers that should be in this set + Replicas int32 `json:"replicas"` + // Template the GameServer template to apply for this GameServerSet + Template GameServerTemplateSpec `json:"template"` +} + +// GameServerSetStatus is the status of a GameServerSet +type GameServerSetStatus struct { + // Replicas the total number of current GameServer replicas + Replicas int32 `json:"replicas"` + // ReadyReplicas are the number of Ready GameServer replicas + ReadyReplicas int32 `json:"readyReplicas"` +} + +// ValidateUpdate validates when updates occur. The argument +// is the new GameServerSet, being passed into the old GameServerSet +func (gsSet *GameServerSet) ValidateUpdate(new *GameServerSet) (bool, []metav1.StatusCause) { + var causes []metav1.StatusCause + if !reflect.DeepEqual(gsSet.Spec.Template, new.Spec.Template) { + causes = append(causes, metav1.StatusCause{ + Type: metav1.CauseTypeFieldValueInvalid, + Field: "template", + Message: "template values cannot be updated after creation", + }) + } + + return len(causes) == 0, causes +} + +// GameServer returns a single GameServer derived +// from the GameSever template +func (gsSet *GameServerSet) GameServer() *GameServer { + gs := &GameServer{ + ObjectMeta: *gsSet.Spec.Template.ObjectMeta.DeepCopy(), + Spec: *gsSet.Spec.Template.Spec.DeepCopy(), + } + + // Switch to GenerateName, so that we always get a Unique name for the GameServer, and there + // can be no collisions + gs.ObjectMeta.GenerateName = gsSet.ObjectMeta.Name + "-" + gs.ObjectMeta.Name = "" + gs.ObjectMeta.Namespace = gsSet.ObjectMeta.Namespace + gs.ObjectMeta.ResourceVersion = "" + gs.ObjectMeta.UID = "" + + ref := metav1.NewControllerRef(gsSet, SchemeGroupVersion.WithKind("GameServerSet")) + gs.ObjectMeta.OwnerReferences = append(gs.ObjectMeta.OwnerReferences, *ref) + + if gs.ObjectMeta.Labels == nil { + gs.ObjectMeta.Labels = make(map[string]string, 1) + } + + gs.ObjectMeta.Labels[GameServerSetGameServerLabel] = gsSet.ObjectMeta.Name + + return gs +} diff --git a/pkg/apis/stable/v1alpha1/gameserverset_test.go b/pkg/apis/stable/v1alpha1/gameserverset_test.go new file mode 100644 index 0000000000..d2c37e32e9 --- /dev/null +++ b/pkg/apis/stable/v1alpha1/gameserverset_test.go @@ -0,0 +1,82 @@ +// Copyright 2018 Google Inc. 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 v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGameServerSetGameServer(t *testing.T) { + gsSet := GameServerSet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "namespace", + UID: "1234", + }, + Spec: GameServerSetSpec{ + Replicas: 10, + Template: GameServerTemplateSpec{ + Spec: GameServerSpec{ + ContainerPort: 1234, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "container", Image: "myimage"}}, + }, + }, + }, + }, + }, + } + + gs := gsSet.GameServer() + assert.Equal(t, "", gs.ObjectMeta.Name) + assert.Equal(t, gsSet.ObjectMeta.Namespace, gs.ObjectMeta.Namespace) + assert.Equal(t, gsSet.ObjectMeta.Name+"-", gs.ObjectMeta.GenerateName) + assert.Equal(t, gsSet.ObjectMeta.Name, gs.ObjectMeta.Labels[GameServerSetGameServerLabel]) + assert.Equal(t, gs.Spec, gsSet.Spec.Template.Spec) + assert.True(t, v1.IsControlledBy(gs, &gsSet)) +} + +func TestGameServerSetValidateUpdate(t *testing.T) { + gsSet := GameServerSet{ + ObjectMeta: v1.ObjectMeta{Name: "test"}, + Spec: GameServerSetSpec{ + Replicas: 10, + Template: GameServerTemplateSpec{ + Spec: GameServerSpec{ContainerPort: 1234}, + }, + }, + } + + ok, causes := gsSet.ValidateUpdate(gsSet.DeepCopy()) + assert.True(t, ok) + assert.Empty(t, causes) + + newGSS := gsSet.DeepCopy() + newGSS.Spec.Replicas = 5 + ok, causes = gsSet.ValidateUpdate(newGSS) + assert.True(t, ok) + assert.Empty(t, causes) + + newGSS.Spec.Template.Spec.ContainerPort = 321 + ok, causes = gsSet.ValidateUpdate(newGSS) + assert.False(t, ok) + assert.Len(t, causes, 1) + assert.Equal(t, "template", causes[0].Field) +} diff --git a/pkg/apis/stable/v1alpha1/register.go b/pkg/apis/stable/v1alpha1/register.go index 56af76902f..bd82cff3a8 100644 --- a/pkg/apis/stable/v1alpha1/register.go +++ b/pkg/apis/stable/v1alpha1/register.go @@ -52,6 +52,8 @@ func addKnownTypes(scheme *k8sruntime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &GameServer{}, &GameServerList{}, + &GameServerSet{}, + &GameServerSetList{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) return nil diff --git a/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go index 0f40359c3c..689331671f 100644 --- a/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go @@ -87,6 +87,102 @@ func (in *GameServerList) DeepCopyObject() runtime.Object { } } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (gsSet *GameServerSet) DeepCopyInto(out *GameServerSet) { + *out = *gsSet + out.TypeMeta = gsSet.TypeMeta + gsSet.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + gsSet.Spec.DeepCopyInto(&out.Spec) + out.Status = gsSet.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerSet. +func (gsSet *GameServerSet) DeepCopy() *GameServerSet { + if gsSet == nil { + return nil + } + out := new(GameServerSet) + gsSet.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (gsSet *GameServerSet) DeepCopyObject() runtime.Object { + if c := gsSet.DeepCopy(); c != nil { + return c + } else { + return nil + } +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GameServerSetList) DeepCopyInto(out *GameServerSetList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GameServerSet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerSetList. +func (in *GameServerSetList) DeepCopy() *GameServerSetList { + if in == nil { + return nil + } + out := new(GameServerSetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GameServerSetList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } else { + return nil + } +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GameServerSetSpec) DeepCopyInto(out *GameServerSetSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerSetSpec. +func (in *GameServerSetSpec) DeepCopy() *GameServerSetSpec { + if in == nil { + return nil + } + out := new(GameServerSetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GameServerSetStatus) DeepCopyInto(out *GameServerSetStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerSetStatus. +func (in *GameServerSetStatus) DeepCopy() *GameServerSetStatus { + if in == nil { + return nil + } + out := new(GameServerSetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GameServerSpec) DeepCopyInto(out *GameServerSpec) { *out = *in @@ -121,6 +217,24 @@ func (in *GameServerStatus) DeepCopy() *GameServerStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GameServerTemplateSpec) DeepCopyInto(out *GameServerTemplateSpec) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerTemplateSpec. +func (in *GameServerTemplateSpec) DeepCopy() *GameServerTemplateSpec { + if in == nil { + return nil + } + out := new(GameServerTemplateSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Health) DeepCopyInto(out *Health) { *out = *in diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_gameserverset.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_gameserverset.go new file mode 100644 index 0000000000..19e4901de8 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_gameserverset.go @@ -0,0 +1,125 @@ +// Copyright 2018 Google Inc. 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. + +// This code was autogenerated. Do not edit directly. +package fake + +import ( + v1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeGameServerSets implements GameServerSetInterface +type FakeGameServerSets struct { + Fake *FakeStableV1alpha1 + ns string +} + +var gameserversetsResource = schema.GroupVersionResource{Group: "stable.agones.dev", Version: "v1alpha1", Resource: "gameserversets"} + +var gameserversetsKind = schema.GroupVersionKind{Group: "stable.agones.dev", Version: "v1alpha1", Kind: "GameServerSet"} + +// Get takes name of the gameServerSet, and returns the corresponding gameServerSet object, and an error if there is any. +func (c *FakeGameServerSets) Get(name string, options v1.GetOptions) (result *v1alpha1.GameServerSet, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(gameserversetsResource, c.ns, name), &v1alpha1.GameServerSet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GameServerSet), err +} + +// List takes label and field selectors, and returns the list of GameServerSets that match those selectors. +func (c *FakeGameServerSets) List(opts v1.ListOptions) (result *v1alpha1.GameServerSetList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(gameserversetsResource, gameserversetsKind, c.ns, opts), &v1alpha1.GameServerSetList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.GameServerSetList{} + for _, item := range obj.(*v1alpha1.GameServerSetList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested gameServerSets. +func (c *FakeGameServerSets) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(gameserversetsResource, c.ns, opts)) + +} + +// Create takes the representation of a gameServerSet and creates it. Returns the server's representation of the gameServerSet, and an error, if there is any. +func (c *FakeGameServerSets) Create(gameServerSet *v1alpha1.GameServerSet) (result *v1alpha1.GameServerSet, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(gameserversetsResource, c.ns, gameServerSet), &v1alpha1.GameServerSet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GameServerSet), err +} + +// Update takes the representation of a gameServerSet and updates it. Returns the server's representation of the gameServerSet, and an error, if there is any. +func (c *FakeGameServerSets) Update(gameServerSet *v1alpha1.GameServerSet) (result *v1alpha1.GameServerSet, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(gameserversetsResource, c.ns, gameServerSet), &v1alpha1.GameServerSet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GameServerSet), err +} + +// Delete takes name of the gameServerSet and deletes it. Returns an error if one occurs. +func (c *FakeGameServerSets) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(gameserversetsResource, c.ns, name), &v1alpha1.GameServerSet{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeGameServerSets) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(gameserversetsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.GameServerSetList{}) + return err +} + +// Patch applies the patch and returns the patched gameServerSet. +func (c *FakeGameServerSets) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.GameServerSet, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(gameserversetsResource, c.ns, name, data, subresources...), &v1alpha1.GameServerSet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.GameServerSet), err +} diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_stable_client.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_stable_client.go index 921fe9f7d4..f0681bea1f 100644 --- a/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_stable_client.go +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_stable_client.go @@ -29,6 +29,10 @@ func (c *FakeStableV1alpha1) GameServers(namespace string) v1alpha1.GameServerIn return &FakeGameServers{c, namespace} } +func (c *FakeStableV1alpha1) GameServerSets(namespace string) v1alpha1.GameServerSetInterface { + return &FakeGameServerSets{c, namespace} +} + // RESTClient returns a RESTClient that is used to communicate // with API server by this client implementation. func (c *FakeStableV1alpha1) RESTClient() rest.Interface { diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/gameserverset.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/gameserverset.go new file mode 100644 index 0000000000..652127d6b6 --- /dev/null +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/gameserverset.go @@ -0,0 +1,154 @@ +// Copyright 2018 Google Inc. 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. + +// This code was autogenerated. Do not edit directly. +package v1alpha1 + +import ( + v1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + scheme "agones.dev/agones/pkg/client/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// GameServerSetsGetter has a method to return a GameServerSetInterface. +// A group's client should implement this interface. +type GameServerSetsGetter interface { + GameServerSets(namespace string) GameServerSetInterface +} + +// GameServerSetInterface has methods to work with GameServerSet resources. +type GameServerSetInterface interface { + Create(*v1alpha1.GameServerSet) (*v1alpha1.GameServerSet, error) + Update(*v1alpha1.GameServerSet) (*v1alpha1.GameServerSet, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.GameServerSet, error) + List(opts v1.ListOptions) (*v1alpha1.GameServerSetList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.GameServerSet, err error) + GameServerSetExpansion +} + +// gameServerSets implements GameServerSetInterface +type gameServerSets struct { + client rest.Interface + ns string +} + +// newGameServerSets returns a GameServerSets +func newGameServerSets(c *StableV1alpha1Client, namespace string) *gameServerSets { + return &gameServerSets{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the gameServerSet, and returns the corresponding gameServerSet object, and an error if there is any. +func (c *gameServerSets) Get(name string, options v1.GetOptions) (result *v1alpha1.GameServerSet, err error) { + result = &v1alpha1.GameServerSet{} + err = c.client.Get(). + Namespace(c.ns). + Resource("gameserversets"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of GameServerSets that match those selectors. +func (c *gameServerSets) List(opts v1.ListOptions) (result *v1alpha1.GameServerSetList, err error) { + result = &v1alpha1.GameServerSetList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("gameserversets"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested gameServerSets. +func (c *gameServerSets) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("gameserversets"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a gameServerSet and creates it. Returns the server's representation of the gameServerSet, and an error, if there is any. +func (c *gameServerSets) Create(gameServerSet *v1alpha1.GameServerSet) (result *v1alpha1.GameServerSet, err error) { + result = &v1alpha1.GameServerSet{} + err = c.client.Post(). + Namespace(c.ns). + Resource("gameserversets"). + Body(gameServerSet). + Do(). + Into(result) + return +} + +// Update takes the representation of a gameServerSet and updates it. Returns the server's representation of the gameServerSet, and an error, if there is any. +func (c *gameServerSets) Update(gameServerSet *v1alpha1.GameServerSet) (result *v1alpha1.GameServerSet, err error) { + result = &v1alpha1.GameServerSet{} + err = c.client.Put(). + Namespace(c.ns). + Resource("gameserversets"). + Name(gameServerSet.Name). + Body(gameServerSet). + Do(). + Into(result) + return +} + +// Delete takes name of the gameServerSet and deletes it. Returns an error if one occurs. +func (c *gameServerSets) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("gameserversets"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *gameServerSets) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("gameserversets"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched gameServerSet. +func (c *gameServerSets) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.GameServerSet, err error) { + result = &v1alpha1.GameServerSet{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("gameserversets"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go index 582bc0ccd1..69f3013015 100644 --- a/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go @@ -16,3 +16,5 @@ package v1alpha1 type GameServerExpansion interface{} + +type GameServerSetExpansion interface{} diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go index 1bb994e211..7b57ac2a08 100644 --- a/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go @@ -25,6 +25,7 @@ import ( type StableV1alpha1Interface interface { RESTClient() rest.Interface GameServersGetter + GameServerSetsGetter } // StableV1alpha1Client is used to interact with features provided by the stable.agones.dev group. @@ -36,6 +37,10 @@ func (c *StableV1alpha1Client) GameServers(namespace string) GameServerInterface return newGameServers(c, namespace) } +func (c *StableV1alpha1Client) GameServerSets(namespace string) GameServerSetInterface { + return newGameServerSets(c, namespace) +} + // NewForConfig creates a new StableV1alpha1Client for the given config. func NewForConfig(c *rest.Config) (*StableV1alpha1Client, error) { config := *c diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index c89adfc914..78fde1793f 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -55,6 +55,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource // Group=stable.agones.dev, Version=v1alpha1 case v1alpha1.SchemeGroupVersion.WithResource("gameservers"): return &genericInformer{resource: resource.GroupResource(), informer: f.Stable().V1alpha1().GameServers().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("gameserversets"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Stable().V1alpha1().GameServerSets().Informer()}, nil } diff --git a/pkg/client/informers/externalversions/stable/v1alpha1/gameserverset.go b/pkg/client/informers/externalversions/stable/v1alpha1/gameserverset.go new file mode 100644 index 0000000000..1e6061ce0c --- /dev/null +++ b/pkg/client/informers/externalversions/stable/v1alpha1/gameserverset.go @@ -0,0 +1,89 @@ +// Copyright 2018 Google Inc. 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. + +// This code was autogenerated. Do not edit directly. + +// This file was automatically generated by informer-gen + +package v1alpha1 + +import ( + time "time" + + stable_v1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + versioned "agones.dev/agones/pkg/client/clientset/versioned" + internalinterfaces "agones.dev/agones/pkg/client/informers/externalversions/internalinterfaces" + v1alpha1 "agones.dev/agones/pkg/client/listers/stable/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// GameServerSetInformer provides access to a shared informer and lister for +// GameServerSets. +type GameServerSetInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.GameServerSetLister +} + +type gameServerSetInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewGameServerSetInformer constructs a new informer for GameServerSet type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewGameServerSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredGameServerSetInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredGameServerSetInformer constructs a new informer for GameServerSet type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredGameServerSetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.StableV1alpha1().GameServerSets(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.StableV1alpha1().GameServerSets(namespace).Watch(options) + }, + }, + &stable_v1alpha1.GameServerSet{}, + resyncPeriod, + indexers, + ) +} + +func (f *gameServerSetInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredGameServerSetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *gameServerSetInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&stable_v1alpha1.GameServerSet{}, f.defaultInformer) +} + +func (f *gameServerSetInformer) Lister() v1alpha1.GameServerSetLister { + return v1alpha1.NewGameServerSetLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/stable/v1alpha1/interface.go b/pkg/client/informers/externalversions/stable/v1alpha1/interface.go index b9eeb2fee4..00dea0a3b0 100644 --- a/pkg/client/informers/externalversions/stable/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/stable/v1alpha1/interface.go @@ -26,6 +26,8 @@ import ( type Interface interface { // GameServers returns a GameServerInformer. GameServers() GameServerInformer + // GameServerSets returns a GameServerSetInformer. + GameServerSets() GameServerSetInformer } type version struct { @@ -43,3 +45,8 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList func (v *version) GameServers() GameServerInformer { return &gameServerInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} } + +// GameServerSets returns a GameServerSetInformer. +func (v *version) GameServerSets() GameServerSetInformer { + return &gameServerSetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/client/listers/stable/v1alpha1/expansion_generated.go b/pkg/client/listers/stable/v1alpha1/expansion_generated.go index 0d05436cfb..aea2de3929 100644 --- a/pkg/client/listers/stable/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/stable/v1alpha1/expansion_generated.go @@ -25,3 +25,11 @@ type GameServerListerExpansion interface{} // GameServerNamespaceListerExpansion allows custom methods to be added to // GameServerNamespaceLister. type GameServerNamespaceListerExpansion interface{} + +// GameServerSetListerExpansion allows custom methods to be added to +// GameServerSetLister. +type GameServerSetListerExpansion interface{} + +// GameServerSetNamespaceListerExpansion allows custom methods to be added to +// GameServerSetNamespaceLister. +type GameServerSetNamespaceListerExpansion interface{} diff --git a/pkg/client/listers/stable/v1alpha1/gameserverset.go b/pkg/client/listers/stable/v1alpha1/gameserverset.go new file mode 100644 index 0000000000..e0a8bfccf8 --- /dev/null +++ b/pkg/client/listers/stable/v1alpha1/gameserverset.go @@ -0,0 +1,94 @@ +// Copyright 2018 Google Inc. 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. + +// This code was autogenerated. Do not edit directly. + +// This file was automatically generated by lister-gen + +package v1alpha1 + +import ( + v1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// GameServerSetLister helps list GameServerSets. +type GameServerSetLister interface { + // List lists all GameServerSets in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.GameServerSet, err error) + // GameServerSets returns an object that can list and get GameServerSets. + GameServerSets(namespace string) GameServerSetNamespaceLister + GameServerSetListerExpansion +} + +// gameServerSetLister implements the GameServerSetLister interface. +type gameServerSetLister struct { + indexer cache.Indexer +} + +// NewGameServerSetLister returns a new GameServerSetLister. +func NewGameServerSetLister(indexer cache.Indexer) GameServerSetLister { + return &gameServerSetLister{indexer: indexer} +} + +// List lists all GameServerSets in the indexer. +func (s *gameServerSetLister) List(selector labels.Selector) (ret []*v1alpha1.GameServerSet, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GameServerSet)) + }) + return ret, err +} + +// GameServerSets returns an object that can list and get GameServerSets. +func (s *gameServerSetLister) GameServerSets(namespace string) GameServerSetNamespaceLister { + return gameServerSetNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// GameServerSetNamespaceLister helps list and get GameServerSets. +type GameServerSetNamespaceLister interface { + // List lists all GameServerSets in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.GameServerSet, err error) + // Get retrieves the GameServerSet from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.GameServerSet, error) + GameServerSetNamespaceListerExpansion +} + +// gameServerSetNamespaceLister implements the GameServerSetNamespaceLister +// interface. +type gameServerSetNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all GameServerSets in the indexer for a given namespace. +func (s gameServerSetNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.GameServerSet, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.GameServerSet)) + }) + return ret, err +} + +// Get retrieves the GameServerSet from the indexer for a given namespace and name. +func (s gameServerSetNamespaceLister) Get(name string) (*v1alpha1.GameServerSet, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("gameserverset"), name) + } + return obj.(*v1alpha1.GameServerSet), nil +} diff --git a/pkg/gameserversets/controller.go b/pkg/gameserversets/controller.go new file mode 100644 index 0000000000..00aaa699cd --- /dev/null +++ b/pkg/gameserversets/controller.go @@ -0,0 +1,354 @@ +// Copyright 2018 Google Inc. 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 gameserversets + +import ( + "encoding/json" + + "agones.dev/agones/pkg/apis/stable" + stablev1alpha1 "agones.dev/agones/pkg/apis/stable/v1alpha1" + "agones.dev/agones/pkg/client/clientset/versioned" + getterv1alpha1 "agones.dev/agones/pkg/client/clientset/versioned/typed/stable/v1alpha1" + "agones.dev/agones/pkg/client/informers/externalversions" + listerv1alpha1 "agones.dev/agones/pkg/client/listers/stable/v1alpha1" + "agones.dev/agones/pkg/util/crd" + "agones.dev/agones/pkg/util/runtime" + "agones.dev/agones/pkg/util/webhooks" + "agones.dev/agones/pkg/util/workerqueue" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + admv1beta1 "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" + extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" + "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1beta1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" + "github.com/heptiolabs/healthcheck" +) + +var ( + // ErrNoGameServerSetOwner is returned when a GameServerSet can't be found as an owner + // for a GameServer + ErrNoGameServerSetOwner = errors.New("No GameServerSet owner for this GameServer") +) + +// Controller is a the GameServerSet controller +type Controller struct { + logger *logrus.Entry + crdGetter v1beta1.CustomResourceDefinitionInterface + gameServerGetter getterv1alpha1.GameServersGetter + gameServerLister listerv1alpha1.GameServerLister + gameServerSynced cache.InformerSynced + gameServerSetGetter getterv1alpha1.GameServerSetsGetter + gameServerSetLister listerv1alpha1.GameServerSetLister + gameServerSetSynced cache.InformerSynced + workerqueue *workerqueue.WorkerQueue + recorder record.EventRecorder +} + +// NewController returns a new gameserverset crd controller +func NewController( + wh *webhooks.WebHook, + health healthcheck.Handler, + kubeClient kubernetes.Interface, + extClient extclientset.Interface, + agonesClient versioned.Interface, + agonesInformerFactory externalversions.SharedInformerFactory) *Controller { + + gameServers := agonesInformerFactory.Stable().V1alpha1().GameServers() + gsInformer := gameServers.Informer() + gameServerSets := agonesInformerFactory.Stable().V1alpha1().GameServerSets() + gsSetInformer := gameServerSets.Informer() + + c := &Controller{ + crdGetter: extClient.ApiextensionsV1beta1().CustomResourceDefinitions(), + gameServerGetter: agonesClient.StableV1alpha1(), + gameServerLister: gameServers.Lister(), + gameServerSynced: gsInformer.HasSynced, + gameServerSetGetter: agonesClient.StableV1alpha1(), + gameServerSetLister: gameServerSets.Lister(), + gameServerSetSynced: gsSetInformer.HasSynced, + } + + c.logger = runtime.NewLoggerWithType(c) + c.workerqueue = workerqueue.NewWorkerQueue(c.syncGameServerSet, c.logger, stable.GroupName+".GameServerSetController") + health.AddLivenessCheck("gameserverset-workerqueue", healthcheck.Check(c.workerqueue.Healthy)) + + eventBroadcaster := record.NewBroadcaster() + eventBroadcaster.StartLogging(c.logger.Infof) + eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")}) + c.recorder = eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "gameserverset-controller"}) + + wh.AddHandler("/validate", stablev1alpha1.Kind("GameServerSet"), admv1beta1.Update, c.updateValidationHandler) + + gsSetInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.workerqueue.Enqueue, + UpdateFunc: func(oldObj, newObj interface{}) { + oldGss := oldObj.(*stablev1alpha1.GameServerSet) + newGss := newObj.(*stablev1alpha1.GameServerSet) + if oldGss.Spec.Replicas != newGss.Spec.Replicas { + c.workerqueue.Enqueue(newGss) + } + }, + }) + + gsInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.gameServerEventHandler, + UpdateFunc: func(oldObj, newObj interface{}) { + gs := newObj.(*stablev1alpha1.GameServer) + // ignore if already being deleted + if gs.ObjectMeta.DeletionTimestamp == nil { + c.gameServerEventHandler(gs) + } + }, + DeleteFunc: c.gameServerEventHandler, + }) + + return c +} + +// Run the GameServerSet controller. Will block until stop is closed. +// Runs threadiness number workers to process the rate limited queue +func (c *Controller) Run(threadiness int, stop <-chan struct{}) error { + err := crd.WaitForEstablishedCRD(c.crdGetter, "gameserversets."+stable.GroupName, c.logger) + if err != nil { + return err + } + + c.logger.Info("Wait for cache sync") + if !cache.WaitForCacheSync(stop, c.gameServerSynced, c.gameServerSetSynced) { + return errors.New("failed to wait for caches to sync") + } + + c.workerqueue.Run(threadiness, stop) + return nil +} + +// updateValidationHandler that validates a GameServerSet when is updated +// Should only be called on gameserverset update operations. +func (c *Controller) updateValidationHandler(review admv1beta1.AdmissionReview) (admv1beta1.AdmissionReview, error) { + c.logger.WithField("review", review).Info("updateValidationHandler") + + newGss := &stablev1alpha1.GameServerSet{} + oldGss := &stablev1alpha1.GameServerSet{} + + newObj := review.Request.Object + if err := json.Unmarshal(newObj.Raw, newGss); err != nil { + return review, errors.Wrapf(err, "error unmarshalling new GameServerSet json: %s", newObj.Raw) + } + + oldObj := review.Request.OldObject + if err := json.Unmarshal(oldObj.Raw, oldGss); err != nil { + return review, errors.Wrapf(err, "error unmarshalling old GameServerSet json: %s", oldObj.Raw) + } + + ok, causes := oldGss.ValidateUpdate(newGss) + if !ok { + review.Response.Allowed = false + details := metav1.StatusDetails{ + Name: review.Request.Name, + Group: review.Request.Kind.Group, + Kind: review.Request.Kind.Kind, + Causes: causes, + } + review.Response.Result = &metav1.Status{ + Status: metav1.StatusFailure, + Message: "GameServer update is invalid", + Reason: metav1.StatusReasonInvalid, + Details: &details, + } + + c.logger.WithField("review", review).Info("Invalid GameServerSet update") + return review, nil + } + + return review, nil +} + +func (c *Controller) gameServerEventHandler(obj interface{}) { + gs := obj.(*stablev1alpha1.GameServer) + ref := metav1.GetControllerOf(gs) + if ref == nil { + return + } + gsSet, err := c.gameServerSetLister.GameServerSets(gs.ObjectMeta.Namespace).Get(ref.Name) + if err != nil { + if k8serrors.IsNotFound(err) { + c.logger.WithField("ref", ref).Info("Owner GameServerSet no longer available for syncing") + } else { + runtime.HandleError(c.logger.WithField("gs", gs.ObjectMeta.Name).WithField("ref", ref), + errors.Wrap(err, "error retrieving GameServer owner")) + } + return + } + c.workerqueue.Enqueue(gsSet) +} + +// syncGameServer synchronises the GameServers for the Set, +// making sure there are aways as many GameServers as requested +func (c *Controller) syncGameServerSet(key string) error { + c.logger.WithField("key", key).Info("Synchronising") + + // Convert the namespace/name string into a distinct namespace and name + namespace, name, err := cache.SplitMetaNamespaceKey(key) + if err != nil { + // don't return an error, as we don't want this retried + runtime.HandleError(c.logger.WithField("key", key), errors.Wrapf(err, "invalid resource key")) + return nil + } + + gsSet, err := c.gameServerSetLister.GameServerSets(namespace).Get(name) + if err != nil { + if k8serrors.IsNotFound(err) { + c.logger.WithField("key", key).Info("GameServerSet is no longer available for syncing") + return nil + } + return errors.Wrapf(err, "error retrieving GameServerSet %s from namespace %s", name, namespace) + } + + list, err := c.listGameServers(gsSet) + if err != nil { + return err + } + if err := c.syncUnhealthyGameServers(gsSet, list); err != nil { + return err + } + + diff := gsSet.Spec.Replicas - int32(len(list)) + + if err := c.syncMoreGameServers(gsSet, diff); err != nil { + return err + } + if err := c.syncLessGameSevers(gsSet, list, diff); err != nil { + return err + } + if err := c.syncGameServerSetState(gsSet, list); err != nil { + return err + } + + return nil +} + +// listGameServers lists the GameServers for a given GameServerSet +func (c *Controller) listGameServers(gsSet *stablev1alpha1.GameServerSet) ([]*stablev1alpha1.GameServer, error) { + list, err := c.gameServerLister.List(labels.SelectorFromSet(labels.Set{stablev1alpha1.GameServerSetGameServerLabel: gsSet.ObjectMeta.Name})) + if err != nil { + return list, errors.Wrapf(err, "error listing gameservers for gameserverset %s", gsSet.ObjectMeta.Name) + } + + var result []*stablev1alpha1.GameServer + for _, gs := range list { + if metav1.IsControlledBy(gs, gsSet) { + result = append(result, gs) + } + } + + return result, nil +} + +// syncUnhealthyGameServers deletes any unhealthy game servers (that are not already being deleted) +func (c *Controller) syncUnhealthyGameServers(gsSet *stablev1alpha1.GameServerSet, list []*stablev1alpha1.GameServer) error { + for _, gs := range list { + if gs.Status.State == stablev1alpha1.Unhealthy && gs.ObjectMeta.DeletionTimestamp.IsZero() { + err := c.gameServerGetter.GameServers(gs.ObjectMeta.Namespace).Delete(gs.ObjectMeta.Name, nil) + if err != nil { + return errors.Wrapf(err, "error deleting gameserver %s", gs.ObjectMeta.Name) + } + c.recorder.Eventf(gsSet, corev1.EventTypeNormal, "UnhealthyDelete", "Deleted gameserver: %s", gs.ObjectMeta.Name) + } + } + + return nil +} + +// syncMoreGameServers adds diff more GameServers to the set +func (c *Controller) syncMoreGameServers(gsSet *stablev1alpha1.GameServerSet, diff int32) error { + if diff <= 0 { + return nil + } + c.logger.WithField("diff", diff).WithField("gameserverset", gsSet.ObjectMeta.Name).Info("Adding more gameservers") + for i := int32(0); i < diff; i++ { + gs := gsSet.GameServer() + gs, err := c.gameServerGetter.GameServers(gs.Namespace).Create(gs) + if err != nil { + return errors.Wrapf(err, "error creating gameserver for gameserverset %s", gsSet.ObjectMeta.Name) + } + c.recorder.Eventf(gsSet, corev1.EventTypeNormal, "SuccessfulCreate", "Created gameserver: %s", gs.ObjectMeta.Name) + } + + return nil +} + +// syncLessGameSevers removes Ready GameServers from the set of GameServers +func (c *Controller) syncLessGameSevers(gsSet *stablev1alpha1.GameServerSet, list []*stablev1alpha1.GameServer, diff int32) error { + if diff >= 0 { + return nil + } + // easier to manage positive numbers + diff = -diff + c.logger.WithField("diff", diff).WithField("gameserverset", gsSet.ObjectMeta.Name).Info("Deleting gameservers") + count := int32(0) + + // count anything that is already being deleted + for _, gs := range list { + if !gs.ObjectMeta.DeletionTimestamp.IsZero() { + diff-- + } + } + + for _, gs := range list { + if diff <= count { + return nil + } + + if gs.Status.State != stablev1alpha1.Allocated { + err := c.gameServerGetter.GameServers(gs.Namespace).Delete(gs.ObjectMeta.Name, nil) + if err != nil { + return errors.Wrapf(err, "error deleting gameserver for gameserverset %s", gsSet.ObjectMeta.Name) + } + c.recorder.Eventf(gsSet, corev1.EventTypeNormal, "SuccessfulDelete", "Deleted GameServer: %s", gs.ObjectMeta.Name) + count++ + } + } + + return nil +} + +// syncGameServerSetState synchronises the GameServerSet State with active GameServer counts +func (c *Controller) syncGameServerSetState(gsSet *stablev1alpha1.GameServerSet, list []*stablev1alpha1.GameServer) error { + rc := int32(0) + for _, gs := range list { + if gs.Status.State == stablev1alpha1.Ready { + rc++ + } + } + + status := stablev1alpha1.GameServerSetStatus{Replicas: int32(len(list)), ReadyReplicas: rc} + if gsSet.Status != status { + gsSetCopy := gsSet.DeepCopy() + gsSetCopy.Status = status + _, err := c.gameServerSetGetter.GameServerSets(gsSet.ObjectMeta.Namespace).Update(gsSetCopy) + if err != nil { + return errors.Wrapf(err, "error updating status on GameServerSet %s", gsSet.ObjectMeta.Name) + } + } + return nil +} diff --git a/pkg/gameserversets/controller_test.go b/pkg/gameserversets/controller_test.go new file mode 100644 index 0000000000..56cc0a6239 --- /dev/null +++ b/pkg/gameserversets/controller_test.go @@ -0,0 +1,539 @@ +// Copyright 2018 Google Inc. 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 gameserversets + +import ( + "sort" + "strconv" + "testing" + "time" + + "encoding/json" + + "agones.dev/agones/pkg/apis/stable/v1alpha1" + "agones.dev/agones/pkg/util/webhooks" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + admv1beta1 "k8s.io/api/admission/v1beta1" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + k8stesting "k8s.io/client-go/testing" + "k8s.io/client-go/tools/cache" + "github.com/heptiolabs/healthcheck" +) + +func TestControllerWatchGameServers(t *testing.T) { + gsSet := defaultFixture() + + c, m := newFakeController() + + received := make(chan string) + defer close(received) + + m.extClient.AddReactor("get", "customresourcedefinitions", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, newEstablishedCRD(), nil + }) + gsSetWatch := watch.NewFake() + m.agonesClient.AddWatchReactor("gameserversets", k8stesting.DefaultWatchReactor(gsSetWatch, nil)) + gsWatch := watch.NewFake() + m.agonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil)) + + c.workerqueue.SyncHandler = func(name string) error { + received <- name + return nil + } + + stop, cancel := startInformers(m, c.gameServerSynced) + defer cancel() + + go func() { + err := c.Run(1, stop) + assert.Nil(t, err) + }() + + f := func() string { + select { + case result := <-received: + return result + case <-time.After(3 * time.Second): + assert.FailNow(t, "timeout occurred") + } + return "" + } + + expected, err := cache.MetaNamespaceKeyFunc(gsSet) + assert.Nil(t, err) + + // gsSet add + logrus.Info("adding gsSet") + gsSetWatch.Add(gsSet.DeepCopy()) + assert.Nil(t, err) + assert.Equal(t, expected, f()) + // gsSet update + logrus.Info("modify gsSet") + gsSetCopy := gsSet.DeepCopy() + gsSetCopy.Spec.Replicas = 5 + gsSetWatch.Modify(gsSetCopy) + assert.Equal(t, expected, f()) + + gs := gsSet.GameServer() + gs.ObjectMeta.Name = "test-gs" + // gs add + logrus.Info("add gs") + gsWatch.Add(gs.DeepCopy()) + assert.Equal(t, expected, f()) + + // gs update + gsCopy := gs.DeepCopy() + now := metav1.Now() + gsCopy.ObjectMeta.DeletionTimestamp = &now + + logrus.Info("modify gs - noop") + gsWatch.Modify(gsCopy.DeepCopy()) + select { + case <-received: + assert.Fail(t, "Should be no value") + case <-time.After(time.Second): + } + + gsCopy = gs.DeepCopy() + gsCopy.Status.State = v1alpha1.Unhealthy + logrus.Info("modify gs - unhealthy") + gsWatch.Modify(gsCopy.DeepCopy()) + assert.Equal(t, expected, f()) + + // gs delete + logrus.Info("delete gs") + gsWatch.Delete(gsCopy.DeepCopy()) + assert.Equal(t, expected, f()) +} + +func TestSyncGameServerSet(t *testing.T) { + t.Run("adding and deleting unhealthy gameservers", func(t *testing.T) { + gsSet := defaultFixture() + list := createGameServers(gsSet, 5) + + // make some as unhealthy + list[0].Status.State = v1alpha1.Unhealthy + + deleted := false + count := 0 + + c, m := newFakeController() + m.agonesClient.AddReactor("list", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerSetList{Items: []v1alpha1.GameServerSet{*gsSet}}, nil + }) + m.agonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerList{Items: list}, nil + }) + + m.agonesClient.AddReactor("delete", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + da := action.(k8stesting.DeleteAction) + deleted = true + assert.Equal(t, "test-0", da.GetName()) + return true, nil, nil + }) + m.agonesClient.AddReactor("create", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + ca := action.(k8stesting.CreateAction) + gs := ca.GetObject().(*v1alpha1.GameServer) + + assert.True(t, metav1.IsControlledBy(gs, gsSet)) + count++ + return true, gs, nil + }) + + _, cancel := startInformers(m, c.gameServerSetSynced, c.gameServerSynced) + defer cancel() + + c.syncGameServerSet(gsSet.ObjectMeta.Namespace + "/" + gsSet.ObjectMeta.Name) + + assert.Equal(t, 5, count) + assert.True(t, deleted, "A game servers should have been deleted") + }) + + t.Run("removing gamservers", func(t *testing.T) { + gsSet := defaultFixture() + list := createGameServers(gsSet, 15) + count := 0 + + c, m := newFakeController() + m.agonesClient.AddReactor("list", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerSetList{Items: []v1alpha1.GameServerSet{*gsSet}}, nil + }) + m.agonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerList{Items: list}, nil + }) + m.agonesClient.AddReactor("delete", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + count++ + return true, nil, nil + }) + + _, cancel := startInformers(m, c.gameServerSetSynced, c.gameServerSynced) + defer cancel() + + c.syncGameServerSet(gsSet.ObjectMeta.Namespace + "/" + gsSet.ObjectMeta.Name) + + assert.Equal(t, 5, count) + }) +} + +func TestControllerListGameServers(t *testing.T) { + gsSet := defaultFixture() + + gs1 := gsSet.GameServer() + gs1.ObjectMeta.Name = "test-1" + gs2 := gsSet.GameServer() + assert.True(t, metav1.IsControlledBy(gs2, gsSet)) + + gs2.ObjectMeta.Name = "test-2" + gs3 := v1alpha1.GameServer{ObjectMeta: metav1.ObjectMeta{Name: "not-included"}} + gs4 := gsSet.GameServer() + gs4.ObjectMeta.OwnerReferences = nil + + c, m := newFakeController() + m.agonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerList{Items: []v1alpha1.GameServer{*gs1, *gs2, gs3, *gs4}}, nil + }) + + _, cancel := startInformers(m) + defer cancel() + + list, err := c.listGameServers(gsSet) + assert.Nil(t, err) + + // sort of stable ordering + sort.SliceStable(list, func(i, j int) bool { + return list[i].ObjectMeta.Name < list[j].ObjectMeta.Name + }) + assert.Equal(t, []*v1alpha1.GameServer{gs1, gs2}, list) +} + +func TestControllerSyncUnhealthyGameServers(t *testing.T) { + gsSet := defaultFixture() + + gs1 := gsSet.GameServer() + gs1.ObjectMeta.Name = "test-1" + gs1.Status = v1alpha1.GameServerStatus{State: v1alpha1.Unhealthy} + + gs2 := gsSet.GameServer() + gs2.ObjectMeta.Name = "test-2" + gs2.Status = v1alpha1.GameServerStatus{State: v1alpha1.Ready} + + gs3 := gsSet.GameServer() + gs3.ObjectMeta.Name = "test-3" + now := metav1.Now() + gs3.ObjectMeta.DeletionTimestamp = &now + gs3.Status = v1alpha1.GameServerStatus{State: v1alpha1.Ready} + + deleted := false + + c, m := newFakeController() + m.agonesClient.AddReactor("delete", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + deleted = true + da := action.(k8stesting.DeleteAction) + assert.Equal(t, gs1.ObjectMeta.Name, da.GetName()) + + return true, nil, nil + }) + + _, cancel := startInformers(m) + defer cancel() + + err := c.syncUnhealthyGameServers(gsSet, []*v1alpha1.GameServer{gs1, gs2, gs3}) + assert.Nil(t, err) + + assert.True(t, deleted, "Deletion should have occured") +} + +func TestSyncMoreGameServers(t *testing.T) { + gsSet := defaultFixture() + + c, m := newFakeController() + count := 0 + expected := 10 + + m.agonesClient.AddReactor("create", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + ca := action.(k8stesting.CreateAction) + gs := ca.GetObject().(*v1alpha1.GameServer) + + assert.True(t, metav1.IsControlledBy(gs, gsSet)) + count++ + + return true, gs, nil + }) + + _, cancel := startInformers(m) + defer cancel() + + err := c.syncMoreGameServers(gsSet, int32(expected)) + assert.Nil(t, err) + assert.Equal(t, expected, count) + + select { + case event := <-m.fakeRecorder.Events: + assert.Contains(t, event, "SuccessfulCreate") + case <-time.After(3 * time.Second): + assert.FailNow(t, "should have received an event") + } +} + +func TestSyncLessGameServers(t *testing.T) { + gsSet := defaultFixture() + + c, m := newFakeController() + count := 0 + expected := 5 + + list := createGameServers(gsSet, 11) + + // make some as unhealthy + list[0].Status.State = v1alpha1.Allocated + list[3].Status.State = v1alpha1.Allocated + + // make the last one already being deleted + now := metav1.Now() + list[10].ObjectMeta.DeletionTimestamp = &now + + // gate + assert.Equal(t, v1alpha1.Allocated, list[0].Status.State) + assert.Equal(t, v1alpha1.Allocated, list[3].Status.State) + assert.False(t, list[10].ObjectMeta.DeletionTimestamp.IsZero()) + + m.agonesClient.AddReactor("list", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerList{Items: list}, nil + }) + m.agonesClient.AddReactor("delete", "gameservers", func(action k8stesting.Action) (bool, runtime.Object, error) { + da := action.(k8stesting.DeleteAction) + + found := false + for _, gs := range list { + if gs.ObjectMeta.Name == da.GetName() { + found = true + assert.NotEqual(t, gs.Status.State, v1alpha1.Allocated) + } + } + assert.True(t, found) + count++ + + return true, nil, nil + }) + + _, cancel := startInformers(m) + defer cancel() + + list2, err := c.listGameServers(gsSet) + assert.Nil(t, err) + assert.Len(t, list2, 11) + + err = c.syncLessGameSevers(gsSet, list2, int32(-expected)) + assert.Nil(t, err) + + // subtract one, because one is already deleted + assert.Equal(t, expected-1, count) + + select { + case event := <-m.fakeRecorder.Events: + assert.Contains(t, event, "SuccessfulDelete") + case <-time.After(3 * time.Second): + assert.FailNow(t, "should have received an event") + } +} + +func TestControllerSyncGameServerSetState(t *testing.T) { + t.Parallel() + + t.Run("empty list", func(t *testing.T) { + gsSet := defaultFixture() + c, m := newFakeController() + + updated := false + m.agonesClient.AddReactor("update", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + updated = true + return true, nil, nil + }) + + err := c.syncGameServerSetState(gsSet, nil) + assert.Nil(t, err) + assert.False(t, updated) + }) + + t.Run("all ready list", func(t *testing.T) { + gsSet := defaultFixture() + c, m := newFakeController() + + updated := false + m.agonesClient.AddReactor("update", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + updated = true + ua := action.(k8stesting.UpdateAction) + gsSet := ua.GetObject().(*v1alpha1.GameServerSet) + + assert.Equal(t, int32(1), gsSet.Status.Replicas) + assert.Equal(t, int32(1), gsSet.Status.ReadyReplicas) + + return true, nil, nil + }) + + list := []*v1alpha1.GameServer{{Status: v1alpha1.GameServerStatus{State: v1alpha1.Ready}}} + err := c.syncGameServerSetState(gsSet, list) + assert.Nil(t, err) + assert.True(t, updated) + }) + + t.Run("only some ready list", func(t *testing.T) { + gsSet := defaultFixture() + c, m := newFakeController() + + updated := false + m.agonesClient.AddReactor("update", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + updated = true + ua := action.(k8stesting.UpdateAction) + gsSet := ua.GetObject().(*v1alpha1.GameServerSet) + + assert.Equal(t, int32(6), gsSet.Status.Replicas) + assert.Equal(t, int32(1), gsSet.Status.ReadyReplicas) + + return true, nil, nil + }) + + list := []*v1alpha1.GameServer{ + {Status: v1alpha1.GameServerStatus{State: v1alpha1.Ready}}, + {Status: v1alpha1.GameServerStatus{State: v1alpha1.Starting}}, + {Status: v1alpha1.GameServerStatus{State: v1alpha1.Unhealthy}}, + {Status: v1alpha1.GameServerStatus{State: v1alpha1.PortAllocation}}, + {Status: v1alpha1.GameServerStatus{State: v1alpha1.Error}}, + {Status: v1alpha1.GameServerStatus{State: v1alpha1.Creating}}, + } + err := c.syncGameServerSetState(gsSet, list) + assert.Nil(t, err) + assert.True(t, updated) + }) +} + +func TestControllerUpdateValidationHandler(t *testing.T) { + t.Parallel() + + c, _ := newFakeController() + gvk := metav1.GroupVersionKind(v1alpha1.SchemeGroupVersion.WithKind("GameServerSet")) + fixture := &v1alpha1.GameServerSet{ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + Spec: v1alpha1.GameServerSetSpec{Replicas: 5}, + } + raw, err := json.Marshal(fixture) + assert.Nil(t, err) + + t.Run("valid gameserverset update", func(t *testing.T) { + new := fixture.DeepCopy() + new.Spec.Replicas = 10 + newRaw, err := json.Marshal(new) + assert.Nil(t, err) + + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: gvk, + Operation: admv1beta1.Create, + Object: runtime.RawExtension{ + Raw: newRaw, + }, + OldObject: runtime.RawExtension{ + Raw: raw, + }, + }, + Response: &admv1beta1.AdmissionResponse{Allowed: true}, + } + + result, err := c.updateValidationHandler(review) + assert.Nil(t, err) + assert.True(t, result.Response.Allowed) + }) + + t.Run("invalid gameserverset update", func(t *testing.T) { + new := fixture.DeepCopy() + new.Spec.Template = v1alpha1.GameServerTemplateSpec{ + Spec: v1alpha1.GameServerSpec{ + PortPolicy: v1alpha1.Static, + }, + } + newRaw, err := json.Marshal(new) + assert.Nil(t, err) + + assert.NotEqual(t, string(raw), string(newRaw)) + + review := admv1beta1.AdmissionReview{ + Request: &admv1beta1.AdmissionRequest{ + Kind: gvk, + Operation: admv1beta1.Create, + Object: runtime.RawExtension{ + Raw: newRaw, + }, + OldObject: runtime.RawExtension{ + Raw: raw, + }, + }, + Response: &admv1beta1.AdmissionResponse{Allowed: true}, + } + + logrus.Info("here?") + result, err := c.updateValidationHandler(review) + assert.Nil(t, err) + assert.False(t, result.Response.Allowed) + assert.Equal(t, metav1.StatusFailure, result.Response.Result.Status) + assert.Equal(t, metav1.StatusReasonInvalid, result.Response.Result.Reason) + }) +} + +// defaultFixture creates the default GameServerSet fixture +func defaultFixture() *v1alpha1.GameServerSet { + gsSet := &v1alpha1.GameServerSet{ + ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "test", UID: "1234"}, + Spec: v1alpha1.GameServerSetSpec{ + Replicas: 10, + Template: v1alpha1.GameServerTemplateSpec{}, + }, + } + return gsSet +} + +// createGameServers create an array of GameServers from the GameServerSet +func createGameServers(gsSet *v1alpha1.GameServerSet, size int) []v1alpha1.GameServer { + var list []v1alpha1.GameServer + for i := 0; i < size; i++ { + gs := gsSet.GameServer() + gs.Name = gs.GenerateName + strconv.Itoa(i) + gs.Status = v1alpha1.GameServerStatus{State: v1alpha1.Ready} + list = append(list, *gs) + } + return list +} + +// newFakeController returns a controller, backed by the fake Clientset +func newFakeController() (*Controller, mocks) { + m := newMocks() + wh := webhooks.NewWebHook("", "") + c := NewController(wh, healthcheck.NewHandler(), m.kubeClient, m.extClient, m.agonesClient, m.agonesInformerFactory) + c.recorder = m.fakeRecorder + return c, m +} + +func newEstablishedCRD() *v1beta1.CustomResourceDefinition { + return &v1beta1.CustomResourceDefinition{ + Status: v1beta1.CustomResourceDefinitionStatus{ + Conditions: []v1beta1.CustomResourceDefinitionCondition{{ + Type: v1beta1.Established, + Status: v1beta1.ConditionTrue, + }}, + }, + } +} diff --git a/pkg/gameserversets/doc.go b/pkg/gameserversets/doc.go new file mode 100644 index 0000000000..08346531fa --- /dev/null +++ b/pkg/gameserversets/doc.go @@ -0,0 +1,17 @@ +// Copyright 2018 Google Inc. 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 gameserversets handles management of the +// GameServerSet Custom Resource Definition +package gameserversets diff --git a/pkg/gameserversets/helper_test.go b/pkg/gameserversets/helper_test.go new file mode 100644 index 0000000000..00487ba72b --- /dev/null +++ b/pkg/gameserversets/helper_test.go @@ -0,0 +1,66 @@ +// Copyright 2018 Google Inc. 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 gameserversets + +import ( + "context" + "time" + + agonesfake "agones.dev/agones/pkg/client/clientset/versioned/fake" + "agones.dev/agones/pkg/client/informers/externalversions" + "github.com/sirupsen/logrus" + extfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" + kubefake "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/tools/cache" + "k8s.io/client-go/tools/record" +) + +// holder for all my fakes and mocks +type mocks struct { + kubeClient *kubefake.Clientset + extClient *extfake.Clientset + agonesClient *agonesfake.Clientset + agonesInformerFactory externalversions.SharedInformerFactory + fakeRecorder *record.FakeRecorder +} + +func newMocks() mocks { + kubeClient := &kubefake.Clientset{} + extClient := &extfake.Clientset{} + agonesClient := &agonesfake.Clientset{} + agonesInformerFactory := externalversions.NewSharedInformerFactory(agonesClient, 30*time.Second) + m := mocks{ + kubeClient: kubeClient, + extClient: extClient, + agonesClient: agonesClient, + agonesInformerFactory: agonesInformerFactory, + fakeRecorder: record.NewFakeRecorder(10), + } + return m +} + +func startInformers(mocks mocks, sync ...cache.InformerSynced) (<-chan struct{}, context.CancelFunc) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + stop := ctx.Done() + + mocks.agonesInformerFactory.Start(stop) + + logrus.Info("Wait for cache sync") + if !cache.WaitForCacheSync(stop, sync...) { + panic("Cache never synced") + } + + return stop, cancel +}