From 26c93658fa6d8728b4080a0faaaaaa52d1a9a158 Mon Sep 17 00:00:00 2001 From: Mark Mandel Date: Wed, 11 Apr 2018 16:15:03 -0700 Subject: [PATCH] Creating a Fleet creates a GameServerSet All this commit is doing is create a GameServerSet when a Fleet is created. Also, if the Replicas in a Fleet are updated, then the owned GameServerSet's Replicas are also updated. For this commit, we are not worrying about handling updates to the Spec (e.g. new image) to the Fleet, and doing updates to a live Fleet, we are just concerned with scaling up and down. Parent ticket: #70 --- build/install.yaml | 0 cmd/controller/main.go | 25 +- examples/cpp-simple/fleet.yaml | 28 ++ examples/simple-udp/server/fleet.yaml | 31 ++ install/helm/agones/templates/crds/fleet.yaml | 41 +++ .../templates/serviceaccounts/controller.yaml | 2 +- install/yaml/install.yaml | 135 +++++++- pkg/apis/stable/v1alpha1/fleet.go | 93 ++++++ pkg/apis/stable/v1alpha1/fleet_test.go | 55 ++++ .../stable/v1alpha1/zz_generated.deepcopy.go | 118 ++++++- .../typed/stable/v1alpha1/fake/fake_fleet.go | 125 ++++++++ .../v1alpha1/fake/fake_stable_client.go | 4 + .../versioned/typed/stable/v1alpha1/fleet.go | 154 +++++++++ .../stable/v1alpha1/generated_expansion.go | 2 + .../typed/stable/v1alpha1/stable_client.go | 5 + .../informers/externalversions/generic.go | 2 + .../externalversions/stable/v1alpha1/fleet.go | 89 ++++++ .../stable/v1alpha1/interface.go | 7 + .../stable/v1alpha1/expansion_generated.go | 8 + pkg/client/listers/stable/v1alpha1/fleet.go | 94 ++++++ pkg/fleets/controller.go | 249 +++++++++++++++ pkg/fleets/controller_test.go | 295 ++++++++++++++++++ pkg/fleets/doc.go | 17 + pkg/gameservers/controller_test.go | 14 +- pkg/gameservers/health_test.go | 2 +- pkg/gameservers/portallocator_test.go | 2 +- pkg/gameservers/sdkserver_test.go | 2 +- pkg/gameserversets/controller_test.go | 16 +- pkg/gameserversets/helper_test.go | 66 ++++ pkg/testing/controller.go | 12 + 30 files changed, 1642 insertions(+), 51 deletions(-) delete mode 100644 build/install.yaml create mode 100644 examples/cpp-simple/fleet.yaml create mode 100644 examples/simple-udp/server/fleet.yaml create mode 100644 install/helm/agones/templates/crds/fleet.yaml create mode 100644 pkg/apis/stable/v1alpha1/fleet.go create mode 100644 pkg/apis/stable/v1alpha1/fleet_test.go create mode 100644 pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleet.go create mode 100644 pkg/client/clientset/versioned/typed/stable/v1alpha1/fleet.go create mode 100644 pkg/client/informers/externalversions/stable/v1alpha1/fleet.go create mode 100644 pkg/client/listers/stable/v1alpha1/fleet.go create mode 100644 pkg/fleets/controller.go create mode 100644 pkg/fleets/controller_test.go create mode 100644 pkg/fleets/doc.go create mode 100644 pkg/gameserversets/helper_test.go diff --git a/build/install.yaml b/build/install.yaml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/cmd/controller/main.go b/cmd/controller/main.go index adfa6c0cc4..e6fca0c4e3 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -25,6 +25,7 @@ import ( "agones.dev/agones/pkg" "agones.dev/agones/pkg/client/clientset/versioned" "agones.dev/agones/pkg/client/informers/externalversions" + "agones.dev/agones/pkg/fleets" "agones.dev/agones/pkg/gameservers" "agones.dev/agones/pkg/gameserversets" "agones.dev/agones/pkg/util/runtime" @@ -41,12 +42,13 @@ import ( ) const ( - sidecarFlag = "sidecar" - pullSidecarFlag = "always-pull-sidecar" - minPortFlag = "min-port" - maxPortFlag = "max-port" - certFileFlag = "cert-file" - keyFileFlag = "key-file" + sidecarFlag = "sidecar" + pullSidecarFlag = "always-pull-sidecar" + minPortFlag = "min-port" + maxPortFlag = "max-port" + certFileFlag = "cert-file" + keyFileFlag = "key-file" + controllerThreadiness = 2 ) var ( @@ -131,6 +133,7 @@ func main() { gsController := gameservers.NewController(wh, health, minPort, maxPort, sidecarImage, alwaysPullSidecar, kubeClient, kubeInformationFactory, extClient, agonesClient, agonesInformerFactory) gsSetController := gameserversets.NewController(wh, health, kubeClient, extClient, agonesClient, agonesInformerFactory) + fleetController := fleets.NewController(health, kubeClient, extClient, agonesClient, agonesInformerFactory) stop := signals.NewStopChannel() @@ -143,17 +146,23 @@ func main() { } }() go func() { - err = gsController.Run(2, stop) + err = gsController.Run(controllerThreadiness, stop) if err != nil { logger.WithError(err).Fatal("Could not run gameserver controller") } }() go func() { - err = gsSetController.Run(2, stop) + err = gsSetController.Run(controllerThreadiness, stop) if err != nil { logger.WithError(err).Fatal("Could not run gameserverset controller") } }() + go func() { + err = fleetController.Run(controllerThreadiness, stop) + if err != nil { + logger.WithError(err).Fatal("Could not run fleet controller") + } + }() go func() { logger.Info("Starting health check...") srv := &http.Server{ diff --git a/examples/cpp-simple/fleet.yaml b/examples/cpp-simple/fleet.yaml new file mode 100644 index 0000000000..b59205157b --- /dev/null +++ b/examples/cpp-simple/fleet.yaml @@ -0,0 +1,28 @@ +# Copyright 2017 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: "stable.agones.dev/v1alpha1" +kind: Fleet +metadata: + # generate a unique name + # will need to be created with `kubectl create` + generateName: cpp-simple- +spec: + containerPort: 7654 + template: + spec: + containers: + - name: cpp-simple + image: gcr.io/agones-images/cpp-simple-server:0.1 + # imagePullPolicy: Always # add for development \ No newline at end of file diff --git a/examples/simple-udp/server/fleet.yaml b/examples/simple-udp/server/fleet.yaml new file mode 100644 index 0000000000..6971c79746 --- /dev/null +++ b/examples/simple-udp/server/fleet.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: Fleet +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/fleet.yaml b/install/helm/agones/templates/crds/fleet.yaml new file mode 100644 index 0000000000..4648fee42f --- /dev/null +++ b/install/helm/agones/templates/crds/fleet.yaml @@ -0,0 +1,41 @@ +# 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: fleets.stable.agones.dev +spec: + group: stable.agones.dev + version: v1alpha1 + scope: Namespaced + names: + kind: Fleet + plural: fleets + shortNames: + - flt + singular: fleet + 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 3d4c6ae872..497f31e9c3 100644 --- a/install/helm/agones/templates/serviceaccounts/controller.yaml +++ b/install/helm/agones/templates/serviceaccounts/controller.yaml @@ -47,7 +47,7 @@ rules: resources: ["customresourcedefinitions"] verbs: ["get"] - apiGroups: ["stable.agones.dev"] - resources: ["gameservers", "gameserversets"] + resources: ["gameservers", "gameserversets", "fleets"] verbs: ["create", "delete", "get", "list", "update", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 diff --git a/install/yaml/install.yaml b/install/yaml/install.yaml index 58639cd4f4..c87ea21db2 100644 --- a/install/yaml/install.yaml +++ b/install/yaml/install.yaml @@ -74,7 +74,7 @@ rules: resources: ["customresourcedefinitions"] verbs: ["get"] - apiGroups: ["stable.agones.dev"] - resources: ["gameservers", "gameserversets"] + resources: ["gameservers", "gameserversets", "fleets"] verbs: ["create", "delete", "get", "list", "update", "watch"] --- apiVersion: rbac.authorization.k8s.io/v1 @@ -156,6 +156,139 @@ roleRef: kind: ClusterRole name: agones-sdk --- +# Source: agones/templates/crds/fleet.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: fleets.stable.agones.dev +spec: + group: stable.agones.dev + version: v1alpha1 + scope: Namespaced + names: + kind: Fleet + plural: fleets + shortNames: + - flt + singular: fleet + 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/crds/gameserver.yaml # Copyright 2018 Google Inc. All Rights Reserved. # diff --git a/pkg/apis/stable/v1alpha1/fleet.go b/pkg/apis/stable/v1alpha1/fleet.go new file mode 100644 index 0000000000..0d0f48de74 --- /dev/null +++ b/pkg/apis/stable/v1alpha1/fleet.go @@ -0,0 +1,93 @@ +// 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 ( + "agones.dev/agones/pkg/apis/stable" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + FleetGameServerSetLabel = stable.GroupName + "/fleet" +) + +// +genclient +// +genclient:noStatus +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Fleet is the data structure for a gameserver resource +type Fleet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec FleetSpec `json:"spec"` + Status FleetStatus `json:"status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// FleetList is a list of GameServer resources +type FleetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []Fleet `json:"items"` +} + +// FleetSpec is the spec for a Fleet +type FleetSpec 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 Fleet + Template GameServerTemplateSpec `json:"template"` +} + +// FleetStatus is the status of a GameServerSet +type FleetStatus 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"` +} + +// Fleet returns a single GameServerSet for this Fleet definition +func (f *Fleet) GameServerSet() *GameServerSet { + gsSet := &GameServerSet{ + ObjectMeta: *f.Spec.Template.ObjectMeta.DeepCopy(), + Spec: GameServerSetSpec{ + Replicas: f.Spec.Replicas, + Template: f.Spec.Template, + }, + } + + // Switch to GenerateName, so that we always get a Unique name for the GameServerSet, and there + // can be no collisions + gsSet.ObjectMeta.GenerateName = f.ObjectMeta.Name + "-" + gsSet.ObjectMeta.Name = "" + gsSet.ObjectMeta.Namespace = f.ObjectMeta.Namespace + gsSet.ObjectMeta.ResourceVersion = "" + gsSet.ObjectMeta.UID = "" + + ref := metav1.NewControllerRef(f, SchemeGroupVersion.WithKind("Fleet")) + gsSet.ObjectMeta.OwnerReferences = append(gsSet.ObjectMeta.OwnerReferences, *ref) + + if gsSet.ObjectMeta.Labels == nil { + gsSet.ObjectMeta.Labels = make(map[string]string, 1) + } + + gsSet.ObjectMeta.Labels[FleetGameServerSetLabel] = f.ObjectMeta.Name + + return gsSet +} diff --git a/pkg/apis/stable/v1alpha1/fleet_test.go b/pkg/apis/stable/v1alpha1/fleet_test.go new file mode 100644 index 0000000000..130bfe61cc --- /dev/null +++ b/pkg/apis/stable/v1alpha1/fleet_test.go @@ -0,0 +1,55 @@ +// 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 TestFleetGameServerSetGameServer(t *testing.T) { + f := Fleet{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "namespace", + UID: "1234", + }, + Spec: FleetSpec{ + Replicas: 10, + Template: GameServerTemplateSpec{ + Spec: GameServerSpec{ + ContainerPort: 1234, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{Name: "container", Image: "myimage"}}, + }, + }, + }, + }, + }, + } + + gsSet := f.GameServerSet() + assert.Equal(t, "", gsSet.ObjectMeta.Name) + assert.Equal(t, f.ObjectMeta.Namespace, gsSet.ObjectMeta.Namespace) + assert.Equal(t, f.ObjectMeta.Name+"-", gsSet.ObjectMeta.GenerateName) + assert.Equal(t, f.ObjectMeta.Name, gsSet.ObjectMeta.Labels[FleetGameServerSetLabel]) + assert.Equal(t, f.Spec.Replicas, gsSet.Spec.Replicas) + assert.Equal(t, f.Spec.Template, gsSet.Spec.Template) + assert.True(t, v1.IsControlledBy(gsSet, &f)) +} diff --git a/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go index 689331671f..5d5e5c3b41 100644 --- a/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/stable/v1alpha1/zz_generated.deepcopy.go @@ -24,6 +24,102 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Fleet) DeepCopyInto(out *Fleet) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Fleet. +func (in *Fleet) DeepCopy() *Fleet { + if in == nil { + return nil + } + out := new(Fleet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Fleet) 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 *FleetList) DeepCopyInto(out *FleetList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Fleet, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetList. +func (in *FleetList) DeepCopy() *FleetList { + if in == nil { + return nil + } + out := new(FleetList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *FleetList) 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 *FleetSpec) DeepCopyInto(out *FleetSpec) { + *out = *in + in.Template.DeepCopyInto(&out.Template) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetSpec. +func (in *FleetSpec) DeepCopy() *FleetSpec { + if in == nil { + return nil + } + out := new(FleetSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FleetStatus) DeepCopyInto(out *FleetStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FleetStatus. +func (in *FleetStatus) DeepCopy() *FleetStatus { + if in == nil { + return nil + } + out := new(FleetStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GameServer) DeepCopyInto(out *GameServer) { *out = *in @@ -88,28 +184,28 @@ 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 +func (in *GameServerSet) DeepCopyInto(out *GameServerSet) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status return } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GameServerSet. -func (gsSet *GameServerSet) DeepCopy() *GameServerSet { - if gsSet == nil { +func (in *GameServerSet) DeepCopy() *GameServerSet { + if in == nil { return nil } out := new(GameServerSet) - gsSet.DeepCopyInto(out) + in.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 { +func (in *GameServerSet) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { return c } else { return nil diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleet.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleet.go new file mode 100644 index 0000000000..e0f862f1dd --- /dev/null +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fake/fake_fleet.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" +) + +// FakeFleets implements FleetInterface +type FakeFleets struct { + Fake *FakeStableV1alpha1 + ns string +} + +var fleetsResource = schema.GroupVersionResource{Group: "stable.agones.dev", Version: "v1alpha1", Resource: "fleets"} + +var fleetsKind = schema.GroupVersionKind{Group: "stable.agones.dev", Version: "v1alpha1", Kind: "Fleet"} + +// Get takes name of the fleet, and returns the corresponding fleet object, and an error if there is any. +func (c *FakeFleets) Get(name string, options v1.GetOptions) (result *v1alpha1.Fleet, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(fleetsResource, c.ns, name), &v1alpha1.Fleet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Fleet), err +} + +// List takes label and field selectors, and returns the list of Fleets that match those selectors. +func (c *FakeFleets) List(opts v1.ListOptions) (result *v1alpha1.FleetList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(fleetsResource, fleetsKind, c.ns, opts), &v1alpha1.FleetList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha1.FleetList{} + for _, item := range obj.(*v1alpha1.FleetList).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 fleets. +func (c *FakeFleets) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(fleetsResource, c.ns, opts)) + +} + +// Create takes the representation of a fleet and creates it. Returns the server's representation of the fleet, and an error, if there is any. +func (c *FakeFleets) Create(fleet *v1alpha1.Fleet) (result *v1alpha1.Fleet, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(fleetsResource, c.ns, fleet), &v1alpha1.Fleet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Fleet), err +} + +// Update takes the representation of a fleet and updates it. Returns the server's representation of the fleet, and an error, if there is any. +func (c *FakeFleets) Update(fleet *v1alpha1.Fleet) (result *v1alpha1.Fleet, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(fleetsResource, c.ns, fleet), &v1alpha1.Fleet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Fleet), err +} + +// Delete takes name of the fleet and deletes it. Returns an error if one occurs. +func (c *FakeFleets) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(fleetsResource, c.ns, name), &v1alpha1.Fleet{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeFleets) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(fleetsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha1.FleetList{}) + return err +} + +// Patch applies the patch and returns the patched fleet. +func (c *FakeFleets) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Fleet, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(fleetsResource, c.ns, name, data, subresources...), &v1alpha1.Fleet{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha1.Fleet), 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 f0681bea1f..41eb46535b 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 @@ -25,6 +25,10 @@ type FakeStableV1alpha1 struct { *testing.Fake } +func (c *FakeStableV1alpha1) Fleets(namespace string) v1alpha1.FleetInterface { + return &FakeFleets{c, namespace} +} + func (c *FakeStableV1alpha1) GameServers(namespace string) v1alpha1.GameServerInterface { return &FakeGameServers{c, namespace} } diff --git a/pkg/client/clientset/versioned/typed/stable/v1alpha1/fleet.go b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fleet.go new file mode 100644 index 0000000000..8f3ab2d0ac --- /dev/null +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/fleet.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" +) + +// FleetsGetter has a method to return a FleetInterface. +// A group's client should implement this interface. +type FleetsGetter interface { + Fleets(namespace string) FleetInterface +} + +// FleetInterface has methods to work with Fleet resources. +type FleetInterface interface { + Create(*v1alpha1.Fleet) (*v1alpha1.Fleet, error) + Update(*v1alpha1.Fleet) (*v1alpha1.Fleet, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha1.Fleet, error) + List(opts v1.ListOptions) (*v1alpha1.FleetList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Fleet, err error) + FleetExpansion +} + +// fleets implements FleetInterface +type fleets struct { + client rest.Interface + ns string +} + +// newFleets returns a Fleets +func newFleets(c *StableV1alpha1Client, namespace string) *fleets { + return &fleets{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the fleet, and returns the corresponding fleet object, and an error if there is any. +func (c *fleets) Get(name string, options v1.GetOptions) (result *v1alpha1.Fleet, err error) { + result = &v1alpha1.Fleet{} + err = c.client.Get(). + Namespace(c.ns). + Resource("fleets"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of Fleets that match those selectors. +func (c *fleets) List(opts v1.ListOptions) (result *v1alpha1.FleetList, err error) { + result = &v1alpha1.FleetList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("fleets"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested fleets. +func (c *fleets) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("fleets"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a fleet and creates it. Returns the server's representation of the fleet, and an error, if there is any. +func (c *fleets) Create(fleet *v1alpha1.Fleet) (result *v1alpha1.Fleet, err error) { + result = &v1alpha1.Fleet{} + err = c.client.Post(). + Namespace(c.ns). + Resource("fleets"). + Body(fleet). + Do(). + Into(result) + return +} + +// Update takes the representation of a fleet and updates it. Returns the server's representation of the fleet, and an error, if there is any. +func (c *fleets) Update(fleet *v1alpha1.Fleet) (result *v1alpha1.Fleet, err error) { + result = &v1alpha1.Fleet{} + err = c.client.Put(). + Namespace(c.ns). + Resource("fleets"). + Name(fleet.Name). + Body(fleet). + Do(). + Into(result) + return +} + +// Delete takes name of the fleet and deletes it. Returns an error if one occurs. +func (c *fleets) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("fleets"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *fleets) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("fleets"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched fleet. +func (c *fleets) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha1.Fleet, err error) { + result = &v1alpha1.Fleet{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("fleets"). + 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 69f3013015..4771f5085e 100644 --- a/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/generated_expansion.go @@ -15,6 +15,8 @@ // This code was autogenerated. Do not edit directly. package v1alpha1 +type FleetExpansion interface{} + 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 7b57ac2a08..401945b176 100644 --- a/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go +++ b/pkg/client/clientset/versioned/typed/stable/v1alpha1/stable_client.go @@ -24,6 +24,7 @@ import ( type StableV1alpha1Interface interface { RESTClient() rest.Interface + FleetsGetter GameServersGetter GameServerSetsGetter } @@ -33,6 +34,10 @@ type StableV1alpha1Client struct { restClient rest.Interface } +func (c *StableV1alpha1Client) Fleets(namespace string) FleetInterface { + return newFleets(c, namespace) +} + func (c *StableV1alpha1Client) GameServers(namespace string) GameServerInterface { return newGameServers(c, namespace) } diff --git a/pkg/client/informers/externalversions/generic.go b/pkg/client/informers/externalversions/generic.go index 78fde1793f..0c5c3683f6 100644 --- a/pkg/client/informers/externalversions/generic.go +++ b/pkg/client/informers/externalversions/generic.go @@ -53,6 +53,8 @@ func (f *genericInformer) Lister() cache.GenericLister { func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { switch resource { // Group=stable.agones.dev, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("fleets"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Stable().V1alpha1().Fleets().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("gameservers"): return &genericInformer{resource: resource.GroupResource(), informer: f.Stable().V1alpha1().GameServers().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("gameserversets"): diff --git a/pkg/client/informers/externalversions/stable/v1alpha1/fleet.go b/pkg/client/informers/externalversions/stable/v1alpha1/fleet.go new file mode 100644 index 0000000000..8f3d954e0b --- /dev/null +++ b/pkg/client/informers/externalversions/stable/v1alpha1/fleet.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" +) + +// FleetInformer provides access to a shared informer and lister for +// Fleets. +type FleetInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha1.FleetLister +} + +type fleetInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewFleetInformer constructs a new informer for Fleet 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 NewFleetInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredFleetInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredFleetInformer constructs a new informer for Fleet 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 NewFilteredFleetInformer(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().Fleets(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.StableV1alpha1().Fleets(namespace).Watch(options) + }, + }, + &stable_v1alpha1.Fleet{}, + resyncPeriod, + indexers, + ) +} + +func (f *fleetInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredFleetInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *fleetInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&stable_v1alpha1.Fleet{}, f.defaultInformer) +} + +func (f *fleetInformer) Lister() v1alpha1.FleetLister { + return v1alpha1.NewFleetLister(f.Informer().GetIndexer()) +} diff --git a/pkg/client/informers/externalversions/stable/v1alpha1/interface.go b/pkg/client/informers/externalversions/stable/v1alpha1/interface.go index 00dea0a3b0..881befccf4 100644 --- a/pkg/client/informers/externalversions/stable/v1alpha1/interface.go +++ b/pkg/client/informers/externalversions/stable/v1alpha1/interface.go @@ -24,6 +24,8 @@ import ( // Interface provides access to all the informers in this group version. type Interface interface { + // Fleets returns a FleetInformer. + Fleets() FleetInformer // GameServers returns a GameServerInformer. GameServers() GameServerInformer // GameServerSets returns a GameServerSetInformer. @@ -41,6 +43,11 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} } +// Fleets returns a FleetInformer. +func (v *version) Fleets() FleetInformer { + return &fleetInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + // GameServers returns a GameServerInformer. func (v *version) GameServers() GameServerInformer { return &gameServerInformer{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 aea2de3929..3f954364fd 100644 --- a/pkg/client/listers/stable/v1alpha1/expansion_generated.go +++ b/pkg/client/listers/stable/v1alpha1/expansion_generated.go @@ -18,6 +18,14 @@ package v1alpha1 +// FleetListerExpansion allows custom methods to be added to +// FleetLister. +type FleetListerExpansion interface{} + +// FleetNamespaceListerExpansion allows custom methods to be added to +// FleetNamespaceLister. +type FleetNamespaceListerExpansion interface{} + // GameServerListerExpansion allows custom methods to be added to // GameServerLister. type GameServerListerExpansion interface{} diff --git a/pkg/client/listers/stable/v1alpha1/fleet.go b/pkg/client/listers/stable/v1alpha1/fleet.go new file mode 100644 index 0000000000..1569e58ee4 --- /dev/null +++ b/pkg/client/listers/stable/v1alpha1/fleet.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" +) + +// FleetLister helps list Fleets. +type FleetLister interface { + // List lists all Fleets in the indexer. + List(selector labels.Selector) (ret []*v1alpha1.Fleet, err error) + // Fleets returns an object that can list and get Fleets. + Fleets(namespace string) FleetNamespaceLister + FleetListerExpansion +} + +// fleetLister implements the FleetLister interface. +type fleetLister struct { + indexer cache.Indexer +} + +// NewFleetLister returns a new FleetLister. +func NewFleetLister(indexer cache.Indexer) FleetLister { + return &fleetLister{indexer: indexer} +} + +// List lists all Fleets in the indexer. +func (s *fleetLister) List(selector labels.Selector) (ret []*v1alpha1.Fleet, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Fleet)) + }) + return ret, err +} + +// Fleets returns an object that can list and get Fleets. +func (s *fleetLister) Fleets(namespace string) FleetNamespaceLister { + return fleetNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// FleetNamespaceLister helps list and get Fleets. +type FleetNamespaceLister interface { + // List lists all Fleets in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha1.Fleet, err error) + // Get retrieves the Fleet from the indexer for a given namespace and name. + Get(name string) (*v1alpha1.Fleet, error) + FleetNamespaceListerExpansion +} + +// fleetNamespaceLister implements the FleetNamespaceLister +// interface. +type fleetNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all Fleets in the indexer for a given namespace. +func (s fleetNamespaceLister) List(selector labels.Selector) (ret []*v1alpha1.Fleet, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha1.Fleet)) + }) + return ret, err +} + +// Get retrieves the Fleet from the indexer for a given namespace and name. +func (s fleetNamespaceLister) Get(name string) (*v1alpha1.Fleet, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha1.Resource("fleet"), name) + } + return obj.(*v1alpha1.Fleet), nil +} diff --git a/pkg/fleets/controller.go b/pkg/fleets/controller.go new file mode 100644 index 0000000000..2a08f75c6b --- /dev/null +++ b/pkg/fleets/controller.go @@ -0,0 +1,249 @@ +// 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 fleets + +import ( + "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/workerqueue" + "github.com/heptiolabs/healthcheck" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + 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" +) + +// Controller is a the GameServerSet controller +type Controller struct { + logger *logrus.Entry + crdGetter v1beta1.CustomResourceDefinitionInterface + gameServerSetGetter getterv1alpha1.GameServerSetsGetter + gameServerSetLister listerv1alpha1.GameServerSetLister + gameServerSetSynced cache.InformerSynced + fleetGetter getterv1alpha1.FleetsGetter + fleetLister listerv1alpha1.FleetLister + fleetSynced cache.InformerSynced + workerqueue *workerqueue.WorkerQueue + recorder record.EventRecorder +} + +// NewController returns a new fleets crd controller +func NewController( + health healthcheck.Handler, + kubeClient kubernetes.Interface, + extClient extclientset.Interface, + agonesClient versioned.Interface, + agonesInformerFactory externalversions.SharedInformerFactory) *Controller { + + gameServerSets := agonesInformerFactory.Stable().V1alpha1().GameServerSets() + gsSetInformer := gameServerSets.Informer() + + fleets := agonesInformerFactory.Stable().V1alpha1().Fleets() + fInformer := fleets.Informer() + + c := &Controller{ + crdGetter: extClient.ApiextensionsV1beta1().CustomResourceDefinitions(), + gameServerSetGetter: agonesClient.StableV1alpha1(), + gameServerSetLister: gameServerSets.Lister(), + gameServerSetSynced: gsSetInformer.HasSynced, + fleetGetter: agonesClient.StableV1alpha1(), + fleetLister: fleets.Lister(), + fleetSynced: fInformer.HasSynced, + } + + c.logger = runtime.NewLoggerWithType(c) + c.workerqueue = workerqueue.NewWorkerQueue(c.syncFleet, c.logger, stable.GroupName+".FleetController") + health.AddLivenessCheck("fleet-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: "fleet-controller"}) + + fInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.workerqueue.Enqueue, + UpdateFunc: func(_, newObj interface{}) { + c.workerqueue.Enqueue(newObj) + }, + }) + + gsSetInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{ + AddFunc: c.gameServerSetEventHandler, + UpdateFunc: func(_, newObj interface{}) { + gsSet := newObj.(*stablev1alpha1.GameServerSet) + // ignore if already being deleted + if gsSet.ObjectMeta.DeletionTimestamp.IsZero() { + c.gameServerSetEventHandler(gsSet) + } + }, + }) + + return c +} + +// Run the Fleet 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, "fleets.stable.agones.dev", c.logger) + if err != nil { + return err + } + + c.logger.Info("Wait for cache sync") + if !cache.WaitForCacheSync(stop, c.gameServerSetSynced, c.fleetSynced) { + return errors.New("failed to wait for caches to sync") + } + + c.workerqueue.Run(threadiness, stop) + return nil +} + +// gameServerSetEventHandler enqueues the owning Fleet for this GameServerSet, +// assuming that it has one +func (c *Controller) gameServerSetEventHandler(obj interface{}) { + gsSet := obj.(*stablev1alpha1.GameServerSet) + ref := metav1.GetControllerOf(gsSet) + if ref == nil { + return + } + + fleet, err := c.fleetLister.Fleets(gsSet.ObjectMeta.Namespace).Get(ref.Name) + if err != nil { + if k8serrors.IsNotFound(err) { + c.logger.WithField("ref", ref).Info("Owner Fleet no longer available for syncing") + } else { + runtime.HandleError(c.logger.WithField("fleet", fleet.ObjectMeta.Name).WithField("ref", ref), + errors.Wrap(err, "error retrieving GameServerSet owner")) + } + return + } + c.workerqueue.Enqueue(fleet) +} + +// syncFleet synchronised the fleet CRDs and configures/updates +// backing GameServerSets +func (c *Controller) syncFleet(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 + } + + fleet, err := c.fleetLister.Fleets(namespace).Get(name) + if err != nil { + if k8serrors.IsNotFound(err) { + c.logger.WithField("key", key).Info("Fleet is no longer available for syncing") + return nil + } + return errors.Wrapf(err, "error retrieving fleet %s from namespace %s", name, namespace) + } + + list, err := c.listGameServerSets(fleet) + if err != nil { + return err + } + + var activeGsSet *stablev1alpha1.GameServerSet + + // if there isn't a GameServerSet yet, then create one + if len(list) == 0 { + activeGsSet = fleet.GameServerSet() + activeGsSet, err = c.gameServerSetGetter.GameServerSets(fleet.ObjectMeta.Namespace).Create(activeGsSet) + if err != nil { + return errors.Wrapf(err, "error creating gameserverset for fleet %s", fleet.ObjectMeta.Name) + } + + c.recorder.Eventf(fleet, corev1.EventTypeNormal, "CreatingGameServerSet", + "Created GameServerSet %s", activeGsSet.ObjectMeta.Name) + + } else { + // for now, we're ignoring any change to the template - will handle on the next PR + // therefore, we are going to assume for the moment, that there is only ever one + // GameServerSet for a Fleet - we will handle multiple GameServerSets in the next PR + activeGsSet = list[0] + } + + // if the replica count has changed, then update the GameServerSet + if fleet.Spec.Replicas != activeGsSet.Spec.Replicas { + gsSetCopy := activeGsSet.DeepCopy() + gsSetCopy.Spec.Replicas = fleet.Spec.Replicas + + if gsSetCopy, err = c.gameServerSetGetter.GameServerSets(fleet.ObjectMeta.Namespace).Update(gsSetCopy); err != nil { + return errors.Wrapf(err, "error updating replicas for gameserverset for fleet %s", fleet.ObjectMeta.Name) + } + c.recorder.Eventf(fleet, corev1.EventTypeNormal, "ScalingGameServerSet", + "Scaling GameServerSet %s to %d", gsSetCopy.ObjectMeta.Name, gsSetCopy.Spec.Replicas) + } + + return c.updateFleetStatus(fleet) +} + +// listGameServerSets lists all the GameServerSets for a given +// Fleet +func (c *Controller) listGameServerSets(f *stablev1alpha1.Fleet) ([]*stablev1alpha1.GameServerSet, error) { + list, err := c.gameServerSetLister.List(labels.SelectorFromSet(labels.Set{stablev1alpha1.FleetGameServerSetLabel: f.ObjectMeta.Name})) + if err != nil { + return list, errors.Wrapf(err, "error listing gameserversets for fleet %s", f.ObjectMeta.Name) + } + + var result []*stablev1alpha1.GameServerSet + for _, gsSet := range list { + if metav1.IsControlledBy(gsSet, f) { + result = append(result, gsSet) + } + } + + return result, nil +} + +// updateFleetStatus gets the GameServerSets for this Fleet and then +// calculates the counts for the status, and updates the Fleet +func (c *Controller) updateFleetStatus(fleet *stablev1alpha1.Fleet) error { + list, err := c.listGameServerSets(fleet) + if err != nil { + return err + } + + fCopy := fleet.DeepCopy() + fCopy.Status.Replicas = 0 + fCopy.Status.ReadyReplicas = 0 + + for _, gsSet := range list { + fCopy.Status.Replicas += gsSet.Status.Replicas + fCopy.Status.ReadyReplicas += gsSet.Status.ReadyReplicas + } + + _, err = c.fleetGetter.Fleets(fCopy.Namespace).Update(fCopy) + return errors.Wrapf(err, "error updating status of fleet %s", fCopy.ObjectMeta.Name) +} diff --git a/pkg/fleets/controller_test.go b/pkg/fleets/controller_test.go new file mode 100644 index 0000000000..a52067ab47 --- /dev/null +++ b/pkg/fleets/controller_test.go @@ -0,0 +1,295 @@ +// 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 fleets + +import ( + "sort" + "testing" + "time" + + "agones.dev/agones/pkg/apis/stable/v1alpha1" + agtesting "agones.dev/agones/pkg/testing" + "github.com/heptiolabs/healthcheck" + "github.com/stretchr/testify/assert" + 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" +) + +func TestControllerListGameServerSets(t *testing.T) { + t.Parallel() + + f := defaultFixture() + + gsSet1 := f.GameServerSet() + gsSet1.ObjectMeta.Name = "gsSet1" + gsSet2 := f.GameServerSet() + gsSet2.ObjectMeta.Name = "gsSet2" + gsSet3 := f.GameServerSet() + gsSet3.ObjectMeta.Name = "gsSet3" + gsSet3.ObjectMeta.Labels = nil + gsSet4 := f.GameServerSet() + gsSet4.ObjectMeta.Name = "gsSet4" + gsSet4.ObjectMeta.OwnerReferences = nil + + c, m := newFakeController() + + m.AgonesClient.AddReactor("list", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerSetList{Items: []v1alpha1.GameServerSet{*gsSet1, *gsSet2, *gsSet3, *gsSet4}}, nil + }) + + _, cancel := agtesting.StartInformers(m) + defer cancel() + + list, err := c.listGameServerSets(f) + 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.GameServerSet{gsSet1, gsSet2}, list) +} + +func TestControllerSyncFleet(t *testing.T) { + t.Parallel() + + t.Run("no gameserverset, create it", func(t *testing.T) { + f := defaultFixture() + c, m := newFakeController() + + created := false + m.AgonesClient.AddReactor("list", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.FleetList{Items: []v1alpha1.Fleet{*f}}, nil + }) + + m.AgonesClient.AddReactor("create", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + ca := action.(k8stesting.CreateAction) + gsSet := ca.GetObject().(*v1alpha1.GameServerSet) + + created = true + assert.True(t, metav1.IsControlledBy(gsSet, f)) + + return true, gsSet, nil + }) + + _, cancel := agtesting.StartInformers(m) + defer cancel() + + err := c.syncFleet("default/fleet-1") + assert.Nil(t, err) + assert.True(t, created, "gameserverset should have been created") + assert.Contains(t, <-m.FakeRecorder.Events, "CreatingGameServerSet") + }) + + t.Run("gamserverset with the same number of replicas", func(t *testing.T) { + t.Parallel() + f := defaultFixture() + c, m := newFakeController() + gsSet := f.GameServerSet() + gsSet.ObjectMeta.Name = "gsSet1" + updated := false + + m.AgonesClient.AddReactor("list", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.FleetList{Items: []v1alpha1.Fleet{*f}}, nil + }) + + 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("update", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + updated = true + return true, nil, nil + }) + + _, cancel := agtesting.StartInformers(m) + defer cancel() + + err := c.syncFleet("default/fleet-1") + assert.Nil(t, err) + assert.False(t, updated, "gameserverset should not have been updated") + + select { + case <-m.FakeRecorder.Events: + assert.FailNow(t, "there should be no events") + case <-time.After(time.Second): + } + }) + + t.Run("gameserverset with different number of replicas", func(t *testing.T) { + f := defaultFixture() + c, m := newFakeController() + gsSet := f.GameServerSet() + gsSet.ObjectMeta.Name = "gsSet1" + gsSet.Spec.Replicas = f.Spec.Replicas + 10 + updated := false + + m.AgonesClient.AddReactor("list", "fleets", func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.FleetList{Items: []v1alpha1.Fleet{*f}}, nil + }) + + 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("update", "gameserversets", func(action k8stesting.Action) (bool, runtime.Object, error) { + updated = true + + ua := action.(k8stesting.UpdateAction) + gsSet := ua.GetObject().(*v1alpha1.GameServerSet) + assert.Equal(t, f.Spec.Replicas, gsSet.Spec.Replicas) + + return true, gsSet, nil + }) + + _, cancel := agtesting.StartInformers(m) + defer cancel() + + err := c.syncFleet("default/fleet-1") + assert.Nil(t, err) + assert.True(t, updated, "gameserverset should have been updated") + assert.Contains(t, <-m.FakeRecorder.Events, "ScalingGameServerSet") + }) +} + +func TestControllerRun(t *testing.T) { + t.Parallel() + + fleet := 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, agtesting.NewEstablishedCRD(), nil + }) + + fleetWatch := watch.NewFake() + m.AgonesClient.AddWatchReactor("fleets", k8stesting.DefaultWatchReactor(fleetWatch, nil)) + + gsSetWatch := watch.NewFake() + m.AgonesClient.AddWatchReactor("gameserversets", k8stesting.DefaultWatchReactor(gsSetWatch, nil)) + + c.workerqueue.SyncHandler = func(name string) error { + received <- name + return nil + } + + stop, cancel := agtesting.StartInformers(m, c.fleetSynced) + 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(fleet) + assert.Nil(t, err) + + // test adding fleet + fleetWatch.Add(fleet.DeepCopy()) + assert.Equal(t, expected, f()) + + // test updating fleet + fCopy := fleet.DeepCopy() + fCopy.Spec.Replicas = fCopy.Spec.Replicas + 10 + fleetWatch.Modify(fCopy) + assert.Equal(t, expected, f()) + + // test add/update of gameserver set + gsSet := fleet.GameServerSet() + gsSet.ObjectMeta.Name = "gs1" + gsSet.ObjectMeta.GenerateName = "" + gsSetWatch.Add(gsSet) + assert.Equal(t, expected, f()) + + gsSet.Spec.Replicas += 10 + gsSetWatch.Modify(gsSet) + assert.Equal(t, expected, f()) +} + +func TestControllerUpdateFleetStatus(t *testing.T) { + t.Parallel() + + fleet := defaultFixture() + c, m := newFakeController() + + gsSet1 := fleet.GameServerSet() + gsSet1.ObjectMeta.Name = "gsSet1" + gsSet1.Status.Replicas = 3 + gsSet1.Status.ReadyReplicas = 2 + + gsSet2 := fleet.GameServerSet() + gsSet2.ObjectMeta.Name = "gsSet2" + gsSet2.Status.Replicas = 5 + gsSet2.Status.ReadyReplicas = 5 + + m.AgonesClient.AddReactor("list", "gameserversets", + func(action k8stesting.Action) (bool, runtime.Object, error) { + return true, &v1alpha1.GameServerSetList{Items: []v1alpha1.GameServerSet{*gsSet1, *gsSet2}}, nil + }) + + m.AgonesClient.AddReactor("update", "fleets", + func(action k8stesting.Action) (bool, runtime.Object, error) { + ua := action.(k8stesting.UpdateAction) + fleet := ua.GetObject().(*v1alpha1.Fleet) + + assert.Equal(t, gsSet1.Status.Replicas+gsSet2.Status.Replicas, fleet.Status.Replicas) + assert.Equal(t, gsSet1.Status.ReadyReplicas+gsSet2.Status.ReadyReplicas, fleet.Status.ReadyReplicas) + return true, fleet, nil + }) + + _, cancel := agtesting.StartInformers(m, c.fleetSynced) + defer cancel() + + err := c.updateFleetStatus(fleet) + assert.Nil(t, err) +} + +// newFakeController returns a controller, backed by the fake Clientset +func newFakeController() (*Controller, agtesting.Mocks) { + m := agtesting.NewMocks() + c := NewController(healthcheck.NewHandler(), m.KubeClient, m.ExtClient, m.AgonesClient, m.AgonesInformerFactory) + c.recorder = m.FakeRecorder + return c, m +} + +func defaultFixture() *v1alpha1.Fleet { + f := &v1alpha1.Fleet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "fleet-1", + Namespace: "default", + UID: "1234", + }, + Spec: v1alpha1.FleetSpec{ + Replicas: 5, + Template: v1alpha1.GameServerTemplateSpec{}, + }, + } + return f +} diff --git a/pkg/fleets/doc.go b/pkg/fleets/doc.go new file mode 100644 index 0000000000..80ba858a9c --- /dev/null +++ b/pkg/fleets/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 fleets handles management of the +// Fleet Custom Resource Definition +package fleets diff --git a/pkg/gameservers/controller_test.go b/pkg/gameservers/controller_test.go index 501bc76980..becf511e1f 100644 --- a/pkg/gameservers/controller_test.go +++ b/pkg/gameservers/controller_test.go @@ -30,7 +30,6 @@ import ( "github.com/stretchr/testify/assert" admv1beta1 "k8s.io/api/admission/v1beta1" corev1 "k8s.io/api/core/v1" - "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -143,7 +142,7 @@ func TestControllerWatchGameServers(t *testing.T) { mocks.AgonesClient.AddWatchReactor("gameservers", k8stesting.DefaultWatchReactor(gsWatch, nil)) mocks.KubeClient.AddWatchReactor("pods", k8stesting.DefaultWatchReactor(podWatch, nil)) mocks.ExtClient.AddReactor("get", "customresourcedefinitions", func(action k8stesting.Action) (bool, runtime.Object, error) { - return true, newEstablishedCRD(), nil + return true, agtesting.NewEstablishedCRD(), nil }) received := make(chan string) @@ -833,14 +832,3 @@ func newFakeController() (*Controller, agtesting.Mocks) { 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/gameservers/health_test.go b/pkg/gameservers/health_test.go index 54dfe1bb47..071aaa163a 100644 --- a/pkg/gameservers/health_test.go +++ b/pkg/gameservers/health_test.go @@ -19,13 +19,13 @@ import ( "time" "agones.dev/agones/pkg/apis/stable/v1alpha1" + agtesting "agones.dev/agones/pkg/testing" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/watch" k8stesting "k8s.io/client-go/testing" - agtesting "agones.dev/agones/pkg/testing" ) func TestHealthControllerFailedContainer(t *testing.T) { diff --git a/pkg/gameservers/portallocator_test.go b/pkg/gameservers/portallocator_test.go index 642523eb29..78ebce192f 100644 --- a/pkg/gameservers/portallocator_test.go +++ b/pkg/gameservers/portallocator_test.go @@ -21,6 +21,7 @@ import ( "sync" "agones.dev/agones/pkg/apis/stable/v1alpha1" + agtesting "agones.dev/agones/pkg/testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" @@ -29,7 +30,6 @@ import ( "k8s.io/apimachinery/pkg/watch" k8stesting "k8s.io/client-go/testing" "k8s.io/client-go/tools/cache" - agtesting "agones.dev/agones/pkg/testing" ) var ( diff --git a/pkg/gameservers/sdkserver_test.go b/pkg/gameservers/sdkserver_test.go index 4b67227ab1..8f222dfe3c 100644 --- a/pkg/gameservers/sdkserver_test.go +++ b/pkg/gameservers/sdkserver_test.go @@ -22,6 +22,7 @@ import ( "agones.dev/agones/pkg/apis/stable/v1alpha1" "agones.dev/agones/pkg/sdk" + agtesting "agones.dev/agones/pkg/testing" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "golang.org/x/net/context" @@ -29,7 +30,6 @@ import ( "k8s.io/apimachinery/pkg/util/clock" "k8s.io/apimachinery/pkg/util/wait" k8stesting "k8s.io/client-go/testing" - agtesting "agones.dev/agones/pkg/testing" ) func TestSidecarRun(t *testing.T) { diff --git a/pkg/gameserversets/controller_test.go b/pkg/gameserversets/controller_test.go index b3bfc6e360..0cbc07cc1e 100644 --- a/pkg/gameserversets/controller_test.go +++ b/pkg/gameserversets/controller_test.go @@ -23,18 +23,17 @@ import ( "encoding/json" "agones.dev/agones/pkg/apis/stable/v1alpha1" + agtesting "agones.dev/agones/pkg/testing" "agones.dev/agones/pkg/util/webhooks" "github.com/heptiolabs/healthcheck" "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" - agtesting "agones.dev/agones/pkg/testing" ) func TestControllerWatchGameServers(t *testing.T) { @@ -46,7 +45,7 @@ func TestControllerWatchGameServers(t *testing.T) { defer close(received) m.ExtClient.AddReactor("get", "customresourcedefinitions", func(action k8stesting.Action) (bool, runtime.Object, error) { - return true, newEstablishedCRD(), nil + return true, agtesting.NewEstablishedCRD(), nil }) gsSetWatch := watch.NewFake() m.AgonesClient.AddWatchReactor("gameserversets", k8stesting.DefaultWatchReactor(gsSetWatch, nil)) @@ -527,14 +526,3 @@ func newFakeController() (*Controller, agtesting.Mocks) { 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/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 +} diff --git a/pkg/testing/controller.go b/pkg/testing/controller.go index f731e96fac..9c50325f53 100644 --- a/pkg/testing/controller.go +++ b/pkg/testing/controller.go @@ -21,6 +21,7 @@ import ( agonesfake "agones.dev/agones/pkg/client/clientset/versioned/fake" "agones.dev/agones/pkg/client/informers/externalversions" "github.com/sirupsen/logrus" + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" extfake "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/fake" "k8s.io/client-go/informers" kubefake "k8s.io/client-go/kubernetes/fake" @@ -69,3 +70,14 @@ func StartInformers(mocks Mocks, sync ...cache.InformerSynced) (<-chan struct{}, return stop, cancel } + +func NewEstablishedCRD() *v1beta1.CustomResourceDefinition { + return &v1beta1.CustomResourceDefinition{ + Status: v1beta1.CustomResourceDefinitionStatus{ + Conditions: []v1beta1.CustomResourceDefinitionCondition{{ + Type: v1beta1.Established, + Status: v1beta1.ConditionTrue, + }}, + }, + } +}